基础
package¶
初始化¶
在一个项目中,如果一个包同时被多个包导入,其 init() 方法只会被执行一次:go 会在程序启动时,自动解析所有包的依赖关系,按照依赖的顺序执行 init()
包级变量可以在函数外部初始化,但是不能使用 := 初始化,不能声明和初始化分离
包导入¶
导入方式 | 作用 |
---|---|
import "pkg" |
正常导入,需显式使用 pkg.Func() |
import _ "pkg" |
仅执行 init() ,不直接使用包内容 |
import . "pkg" |
直接暴露包内容到当前作用域(慎用,易命名冲突) |
import alias "pkg" |
为包设置别名(如 alias.Func() ) |
按路径导入:
例如 git.xxx.com/trpc-go/trpc-naming-consul,这里导入的是整个项目的根目录,根目录下的所有 .go 文件会在同一个包中,这里导入的就是项目的主包。 _ 会执行主包所有的 init() 以及依赖链上的所有包的 init() 方法
注意,属于同一个包的多个 .go 文件中可以分别定义多个 init() 方法
数据类型¶
int¶
直接使用 int 时,int 的范围取决于机器的位数。
- 在 32 位系统上,
int
是 32 位(等价于int32
)。 - 在 64 位系统上,
int
是 64 位(等价于int64
)。
math.MaxInt 也是拿到相应的 \(2^{31} - 1\) 或者 \(2^{63} - 1\)
需要注意的是,int32 和 int64 类型的数据之间的赋值需要显式转换 int32(x)
结构体¶
内存对齐¶
注意 int 和 unint 的大小取决于操作系统的位宽,一个 word 表示操作系统的位宽
在结构体中,一些数据结构的占用位置大小以及对齐单位如下:
type | 大小 | 对齐单位 |
---|---|---|
字符串 | 2 words | 1 word |
指针1 | 1 word | 1 word |
slice | 3 words | 1 word |
map | 1 word | 1 word |
结构体的大小在编译期间完全确定;结构体的单位填充对齐长度为所有字段对齐单位的最大值
注意:slice 占用的空间为其 header 占用的空间,数组的实际内容存储在堆上
通过交换结构体字段的声明位置可以减小空间浪费(编译器不会对字段进行重排序)
Json 编解码¶
编码¶
type Stu struct {
Name string `json:"name"` // 重命名 json key
Age int
HIgh bool
sex string // 非导出字段,不序列化
Class *Class `json:"class"`
}
type Class struct {
Name string
Grade int
}
func main() {
stu := Stu{
Name: "张三",
Age: 18,
HIgh: true,
sex: "男",
}
cla := new(Class)
cla.Name = "1班"
cla.Grade = 3
stu.Class=cla
jsonStu, err := json.Marshal(stu)
if err != nil {
fmt.Println("生成json字符串错误")
}
fmt.Println(string(jsonStu)) // 转为字符串输出
}
输出结果为:
解码¶
非导出字段无法解码,会得到 nil
type Student struct {
Name interface{} `json:"name"`
Age interface{}
sex interface{}
Class interface{}
}
type Class struct {
Name interface{}
GRADE interface{}
}
func main() {
// json 的引号需要用 / 进行转义
data := "{\"name\":\"lively\",\"Age\":10,\"sex\":\"male\",\"Class\":{\"Name\":\"class1\",\"Grade\":10}}"
jsonByte := []byte(data)
stu := Student{}
json.Unmarshal(jsonByte, &stu)
fmt.Println(stu)
}
- 先查找与 key 一样的 json 标签,找到则赋值给该标签对应的变量(如Name)。
- 没有json标签的,就从上往下依次查找变量名与key一样的变量,如Age。或者变量名忽略大小写后与key一样的变量。如GRADE。第一个匹配的就赋值,后面就算有匹配的也忽略。
(前提是该变量必需是可导出的,即首字母大写)。 - 不可导出的变量无法被解析
- 当接收体中存在json串中匹配不了的项时,解析会自动忽略该项,该项仍保留空值
组合¶
Go 通过结构体嵌套实现组合:当将 B 结构体以匿名结构体的形式嵌入结构体 A 时,结构体 A 就获得了结构体 B 的所有方法(和字段)
当进行方法调用时,会先查找当前结构体是否声明了该方法,若没有,再到当前结构体嵌入字段的方法中查找。当多个嵌入字段有重名方法时,需要通过嵌入字段来调用重名方法
var cache = struct {
sync.Mutex // 嵌入
mp map
}{
mp: make(map[string]string)
}
func Query(key string) string {
cache.Lock() // 使用 cache 直接调用绑定 Mutex 结构体的方法
val := cache.mp[key]
cache.UnLock()
return val
}
指针¶
Go 中的指针不可以进行偏移和位运算,因此其指针操作只包括 & 和 *
new¶
返回一个指针,指针的值为该类型的零值
new()
函数在底层使用了 Go 的runtime.newobject
函数。runtime.newobject
函数会分配一块内存,大小为指定类型的大小,并将该内存清零。- 然后,
runtime.newobject
函数会返回这块内存的指针。
new 确实会分配空间,但是对于 map / slice / channel 这样的引用类型,只会得到一个值为 nil 的指向这些引用类型的指针。对于这些引用类型,使用了 new 之后,还是需要使用 make 分配空间
- 例如 new([]int) 只会返回 *slice,但是 slice 结构体的 array 仍然是 nil,len 和 cap 都是 0
make¶
只用于 slice, map, channel 的空间分配,返回类型为类型本身
可寻址与不可寻址¶
- 方法的接收者为 * 即指针类型时,若调用者可寻址,则提供了为非指针类型直接调用的语法糖
- 方法接收者为非指针类型时,无论调用者是何种类型,都会
Go 提供值类型和指针类型间进行转换的语法糖。在值类型向指针类型隐式转换时(比如通过值类型调用绑定指针的方法),需要注意值类型不能是临时的,必须要是可寻址的
不可寻址的值类型:中间的运算 / 取值操作没有赋值给实际的变量
临时的生命周期¶
Go 语言中,变量(如 x := 42
)会在内存中分配一个固定的存储位置,因此可以安全地获取其地址(&x
)。而以下情况的值是临时的、未绑定到变量的,因此不可寻址:
场景 | 是否为临时值? | 解释 |
---|---|---|
Map 元素 | ✅ m["key"] 是临时副本 |
Map 返回的是值的副本,而非原始存储的引用(即使值可修改,地址也不固定)。 |
字符串字节 | ✅ s[0] 是临时字节 |
字符串不可变,取址会破坏不可变性。 |
函数返回值 | ✅ createPerson() 是临时结果 |
返回值未被存储到变量,生命周期仅限于表达式。 |
算术表达式 | ✅ x + 1 是临时计算结果 |
表达式结果未被存储,直接取址无意义。 |
切片操作结果 | ✅ nums[:2][0] 是临时值 |
链式操作生成的中间结果未绑定到变量。 |
Channel 接收值 | ✅ <-ch 是临时接收结果 |
接收的值未被赋值给变量,直接取址无效。 |
使用 := 赋值给变量后,由于是值拷贝,所以可以取址
匿名函数与闭包¶
匿名函数¶
匿名函数没有函数名,需要:
- 保存到某个变量
- 或立刻执行
func main() {
// 将匿名函数保存到变量
add := func(x, y int) {
fmt.Println(x + y)
}
add(10, 20) // 通过变量调用匿名函数
//自执行函数:匿名函数定义完加()直接执行
func(x, y int) {
fmt.Println(x + y)
}(10, 20)
}
闭包¶
闭包 = 函数 + 引用环境
当在一个函数内部定义一个匿名函数(并将其赋值给变量,或作为返回值时),在其生命周期内,其使用的外部函数的变量(通过地址使用)保持有效。即,当匿名函数捕获了外部作用域的变量时,这些变量的生命周期会被延长,直到匿名函数本身不再被引用。
- 只要变量在闭包的同一作用域内(如同一函数内),无论它在闭包之前或之后声明,闭包都可以访问
- 即使外部函数执行结束,被闭包引用的变量仍会存活(逃逸到堆上),所以可以开启一个协程来执行闭包
func calc(base int) (func(int) int, func(int) int) {
add := func(i int) int {
base += i
return base
}
sub := func(i int) int {
base -= i
return base
}
return add, sub
}
func main() {
f1, f2 := calc(10)
fmt.Println(f1(1), f2(2)) //11 9
fmt.Println(f1(3), f2(4)) //12 8
fmt.Println(f1(5), f2(6)) //13 7
}
递归的匿名函数¶
当匿名函数是一个递归函数时,需要先声明一个变量,否则在定义匿名函数时,在定义中调用的函数无法被识别
var visitAll func(items []string) // 必要
visitAll = func(items []string) {
for _, item := range items {
if !seen[item] {
seen[item] = true
visitAll(m[item])
order = append(order, item)
}
}
}
闭包的应用 - option 模式¶
定义了修改结构体属性的函数类型,此类函数会接收外部传入的参数,此参数作为返回函数的闭包变量使用
陷阱¶
通常出现在先保存一个匿名函数,然后操作了上下文中的变量后再调用
在 go 1.22 中,In a "for" statement, each iteration has its own set of iteration variables rather than sharing the same variables in each iteration.
下面的代码需要为每个 tempDirs 中的 dir 创建一个对应的匿名函数,并放到 rmdir 切片中
错误示例:
var rmdirs []func()
for _, dir := range tempDirs {
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir) // 这里记录的是 dir 的地址,只有这个匿名函数真正被调用时,才会从地址中解析出 dir 的实际值,然后通过值传递给 os.RemoveAll()
})
}
for _, rmdir := range rmdirs {
rmdir() // 在实际调用的时候,dir 使用的都是最后一次迭代的值
}
修正:
for _, d := range tempDirs() {
dir := d // 注意这里的 :=
rmdirs = append(rmdirs, func() {
os.RemoveAll(dir)
})
}
这个例子本质上是老版本 for-range 的一个坑:
arr := [2]int{1, 2} res := []*int{} for _, v := range arr { res = append(res, &v) } //expect: 1 2 fmt.Println(*res[0],*res[1]) //but output: 2 2
for-range 会首先拷贝一次待遍历的列表,然后对同一个变量重复赋值,也就是说 v 只会有一个地址。这种情况下,再遍历中基于 v 起一个 goroutine 也是不好的:
在 1.22 版本之前,可能重复输出同一个数字
error¶
定义¶
error 接口是语言的内置类型,在 builtin 包中,定义如下:
只需要绑定了返回 string 的 Error() 方法的结构体就实现了这个接口
errors 包¶
package errors
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
这里 errorString 就实现了 error 接口,errors.New() 方法即可返回一个 errorString 结构体的指针
注意,errors.New() 方法的返回值是一个地址,即使 string 内容相同,地址也不同
当需要比较两个 err 是否是同一个类型时,可以通过比较 err1.Error() == err2.Error() 来比较其字符串是否相同
自定义 error¶
分别设置错误信息和错误码
package myerror
type MyError struct {
code int
msg string
}
func (me MyError) Error() string {
return fmt.Sprintf("code: %d, msg: %s", me.code, me.msg)
}
func New(code int, msg string) error { // 这里返回类型相当于向上转型
return MyError{
code: code,
msg: msg,
}
}
func Code(err error) int {
e, ok := err.(MyError)
if !ok {
return -1
}
return e.code
}
func Msg(err error) string {
e, ok := err.(MyError)
if !ok {
return ""
}
return e.msg
}
time 包¶
time.Time¶
最核心的结构体为 time.Time
基于 time.Time 实例可以获取多种信息:
time.Duration¶
type Duration int64
const (
Nanosecond Duration = 1
Microsecond = 1000 * Nanosecond
Millisecond = 1000 * Microsecond
Second = 1000 * Millisecond
Minute = 60 * Second
Hour = 60 * Minute
)
time 包还定义了一系列常量,如 time.Second 等
定时器¶
可以使用 time.Tick() 来获取一个定时器,使用方式如下:
不会自动释放,有内存泄漏风险