✍ 个人博客:https://blog.csdn.net/Newin2020?type=blog
📝 专栏地址:https://blog.csdn.net/newin2020/category_12898955.html
📣 专栏定位:为 0 基础刚入门 Golang 的小伙伴提供详细的讲解,也欢迎大佬们一起交流~
📚 专栏简介:在这个专栏,我将带着大家从 0 开始入门 Golang 的学习。在这个 Golang 的新人系列专栏下,将会总结 Golang 入门基础的一些知识点,并由浅入深的学习这些知识点,方便大家快速入门学习~
❤️ 如果有收获的话,欢迎点赞 👍 收藏 📁 关注,您的支持就是我创作的最大动力 💪
1. 快速了解
defer 后面的代码会在函数 return 后执行,并且执行的顺序是与代码的顺序相反,即倒序执行。
//main 2 1
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
fmt.Println("main")
return
}
使用 defer 需要注意其执行的时机,以免造成意料之外的影响,例如它可能会修改返回值:
func deferReturn() (ret int) {
defer func() {
ret++
}()
return 10
}
func main() {
ret := deferReturn()
fmt.Printf("ret = %d\r\n",ret) //11
}
2. defer 执行逻辑
我们先来看一段简洁的代码。
func A() {
defer B()
//code to do something
}
上面这段代码,编译后的伪指令是下面这样的。defer 指令对应到两部分内容,其中 deferproc 负责把要执行的函数信息保存起来,我们称之为 defer 注册。而 deferproc 函数会返回 0,下面 if 分支和 panic recover 有关,可以先忽略不看,同时对应要跳转的 ret 这里也先忽略不看。
func A() {
r = deferproc(8, B)
if r > 0 {
goto ret
}
//code to do something
runtime.deferreturn()
return
ret:
runtime.deferreturn()
}
去掉忽略的部分,程序的整体逻辑就比较清晰了。在 defer 注册完成后,程序就会执行后面的逻辑,直到返回之前通过 deferreturn 执行注册的 defer 函数,即 defer 调用。正是因为先注册后调用,才实现了 defer 延迟执行的效果。
func A() {
r = deferproc(8, B) // 1.注册
//code to do something
runtime.deferreturn() // 2.调用
return
}
看回 defer 注册部分,defer 注册的信息会注册到一个链表,而当前执行的 goroutine 会持有这个链表的头指针。每个 goroutine 在运行时都有一个对应的结构体 g,其中有一个字段就指向 defer 链表头。
defer 链表链起来的是一个一个 _defer 结构体,新注册的 defer 会添加到链表头,执行时也是从头开始,这也就是 defer 会表现为倒序执行的原因。
在展开 _defer 结构之前,先看一个例子,这里函数 A 注册了一个 defer 函数 A1。
func A1(a int) {
fmt.Println(a)
}
func A() {
a, b := 1, 2
defer A1(a)
a = a + b
fmt.Println(a, b)
}
我们来看看函数调用栈,A 的栈帧首先会是存放两个局部变量。接着 A1 只有一个参数,因此局部变量下面存放参数 a 的值 1,然后就要注册 defer 函数 A1 了。
deferproc 函数原型只有两个参数,第一个参数是 defer 函数 A1 的参数加返回值共占多大空间。这里 A1 没有返回值,只需要一个整形参数和一个指针变量,因此 64 位下要占 4 字节。
func deferproc (siz int32, fn *funcval)
第二个参数是一个 function value,前面函数部分我们也介绍过,没有捕获列表的 function value 在编译阶段就会做出优化,即在只读数据段分配一个共用的 funcval 结构体,结构体中的指针会指向函数 A1 指令入口,所以 deferproc 的第二个参数就是结构体的地址 addr2。
func deferproc (siz = 4, fn = addr2)
至此我们先把 _defer 的结构体展开了看一下:
type _defer struct {
siz int32 // 参数和返回值共占多少字节,这段空间会直接分配在_defer结构体后面,用于在注册时保存参数,并在执行时拷贝到调用者参数与返回值空间
started bool // 标记defer是否已经执行
sp uintptr // 记录注册这个defer的函数栈指针(调用者栈指针),函数可以通过它判断自己注册的defer是否已经执行完了
pc uintptr // deferproc的返回地址
fn *funcval // 注册的function value函数
_panic *_panic
link *_defer // 链接到前一个注册的defer结构体
}
当 deferproc 函数调用时,编译器会在后面继续开辟一段空间,用于存放 defer 函数的返回值和参数,由于在这个例子里没有返回值,因此只分配 defer 函数的一个参数的空间,这一段空间会被直接拷贝到 _defer 结构体的后面。
另外,返回值地址和调用者函数的 BP 则放在 deferproc 两个参数之后。
在 deferproc 函数执行时,需要堆分配一段空间用于存放 _defer 结构体,而在 _defer 结构体后面也会分配一段空间用于存放 siz 大小的参数与返回值,这里由于没有返回值因此存放参数 a。(注意这里所有的变量存放的顺序是从下至上的,因此参数 a 虽然说是存放在 _defer 结构体的后面,但其实分配的空间在该结构体存放的位置之上)
然后这个 _defer 结构体就会被添加到 defer 链表头,至此 deferproc 注册结束。
_defer 结构体预分配
实际上 go 语言会预分配不同规格的 defer 池,执行时从空闲的 _defer 中取一个出来用即可。如果没有空闲的或者没有大小合适的,则会再进行堆分配,用完以后再放回空闲的 _defer 池,这样就可以避免频繁地堆分配与回收。
让我们再回到函数代码的执行,当代码执行到函数 A 中的 a = a + b 这行代码时,变量 a 被赋值为 3,然后下一步会输出局部变量 a 和 b 的值,即 3 和 2。
接下来就到 deferreturn 执行 defer 链表了,此时会从当前 goroutine 拿到链表头上的这个 _defer 结构体,通过 _defer 结构体里的 fn = addr2 找到对应的 funcval,然后通过 funcval 中的 fn 可以拿到函数入口的地址 addr1。
在调用 A1 时,会把 _defer 后面的参数与返回值整个拷贝到 A1 的调用者栈上,然后 A1 开始执行,此时就会输出 1。
这里的关键是 defer 函数的参数在注册时拷贝到堆上,执行时又拷贝到栈上。并不会去使用到 A 函数栈中保存的局部变量 a 的值 3,所以即使在 defer 函数注册后修改了这个局部变量 a 的值,也不会影响到执行 defer 函数时用到的变量 a。
既然 deferproc 注册的是一个 function value,我们下面就来看看捕获列表时是什么情况,变量 a 在 defer 函数注册后进行修改是否能影响到 defer 函数里使用的变量。
3. defer + 闭包
在下面这个例子中,defer 函数不止要传递局部变量 b 做参数,还捕获了外层函数的局部变量 a 并形成了闭包。
func A() {
a, b := 1, 2
defer func(b int) {
a = a + b
fmt.Println(a, b)
}(b)
a = a + b
fmt.Println(a, b)
}
匿名函数会由编译器按照 A_func1 这样的形式命名。如下图所示,假设这个闭包函数的指令入口地址为 addr1。
由于捕获变量 a 除了初始化赋值外还被修改过,所以局部变量 a 改为堆分配,而栈上存储它的地址。另外,还有一个局部变量 b 也要分配。
然后创建闭包对象,堆分配一个 funcval 结构体,并且捕获列表中存储 a 的地址。
deferproc 执行时,_defer 结构体中的 fn 就是这个 funcval 结构体的起始地址。除此之外,还要拷贝参数 b 的值到 _defer 结构体的后面,然后把这个 _defer 结构体添加到 defer 链表头。
至此,deferproc 注册结束。然后接着执行到 a = a + b 这行代码,变量 a 被赋值为 3。而下一步就自然输出 a 和 b 的变量值,即 3 和 2。
接着就到 deferreturn 了,从 defer 链表头拿到这个 defer 结构体,执行注册的 defer 函数时,需要把参数 b 拷贝到栈上的参数空间。
另外,闭包函数也会通过寄存器存储的 funcval 地址加上偏移,找到捕获变量 a 的地址。
当执行到 defer 函数 A_func1 里的 a = a + b 这行代码时,此时的 a = 3 且 b = 2,所以 a 会被赋值为 5。因此,下一步将会输出变量 a 和 b 的值,即 5 和 2。
可以发现当变量 a 变成被捕获的变量形成闭包后,在注册完 defer 函数后修改变量 a 是可以影响到 defer 函数中使用的变量值的。这是因为此时的变量 a 发生了逃逸,不再分配到栈上而是分配到堆上,defer 函数的变量 a 最终将会从堆上获取具体的值。