一、Defer结构
defer本质其实是一个_defer结构体。
type _defer struct {
siz int32 // 参数和返回值的内存大小
started bool
heap bool // 区分该结构是在栈上分配的,还是对上分配的
sp uintptr // sp 计数器值,栈指针;
pc uintptr // pc 计数器值,程序计数器;
fn *funcval // defer 传入的函数地址,也就是延后执行的函数;
_panic *_panic // panic that is running defer
link *_defer // 链表
}
从defer结构体中可以看到有个成员变量link,他的作用是指向另一个defer结构体,所以多个defer通过link连接,组成一个链表。
而哪里是该链表的表头呢?
在goroutine结构体中也有defer指针的成员变量,其正是defer链表的表头。
二、defer的基本原理
defer逻辑主要分为两步骤:
- 空间分配
- 延迟函数调用
且这两步骤也不是紧挨执行的。
编译器其实会在编译defer deferTestDefer() 这一的时候,会编译成两个函数:
- 分配函数(空间分配,在defer语句出执行)
- 执行函数(延迟调用函数,在return返回后执行)
在1.13之前defer对应的会被编译成两个函数:
- 分配函数:deferproc
- 执行函数:deferreturn
在1.13之后,分配函数增添了deferprocStack函数,因为有了该函数,整体defer的性能提升了30%
- 分配函数:deferproc & deferprocStack
- 执行函数:deferreturn
deferproc和deferprocStack这两个函数区别是什么,为什么增加了deferprocStack函数后,就使得性能提升了呢?
因为deferproc的空间分配是在堆上分配,而deferprocStack则的在栈上。
(后面我们会再详细说关于空间分配的一些细节)
接下来看这段代码
func deferTestDefer(p int)int{
p+=1
return p
}
func defertest(){
defer deferTestDefer(10)
return
}
在编译器编译到 defer deferTestDefer(10)的时候,会先执行分配函数,会将当前defer相关的能用到的空间全都准备好。比如:
- 构造_defer结构体所需要的空间
- 延迟函数deferTestDefer的参数所需要的空间
- 延迟函数deferTestDefer的返回值所需要的空间
由图中可以看出:_defer结构处于这个内存快的头部位置,结构紧跟的是延迟函数的参数和返回值空间,而大小由_defer.siz指定。所以这块内存的值其实是在defer执行的时候(而不是return的时候)就已经填充好了,所以对于在return的时候执行的延迟调用函数,其参数都是预分配的。
注意上述我们所说的几个关键点:
- 延迟调用函数相关的参数,是在执行defer的时候预分配的,而不是在return的时候现分配的;
- 真正执行defer对应的函数,是在return的时候,但是值早就在定义defer的时候已经备好了
问题:defer怎么传递参数
对于defer,延迟回调函数的参数和返回值其实在执行deferprocStack/dederproc的时候就已经分配好了。并不是等到真正执行延迟回调函数的时候才去获取。
go语言中一个函数的参数由caller函数准备好,所以在执行延迟回调函数的时候,deferreturn会把defer延迟回调函数需要的参数准备好(空间和值)
func deferreturn(arg0 uintptr) {
...
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Point