一、defer关键字的说明
defer是一种延迟函数,每增加一个defer函数会将defer函数压栈,所以在执行的时候是后压栈先执行
1、defer触发的场景主要有三
A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking
1)主函数return返回时,在return执行之前
2)主函数执行结束时
3)当前goroutine发生panic
2、官方对defer也提出3条规则
1)延迟函数的参数在defer语句出现时就已经确定下来了
这是因为在defer语句出现的时候,同时把需要的参数拷贝到栈上,后续主函数对该参数进行改变,不会影响defer中的值
1 | func testDerfer() { |
但是指针的情况下,我们可以看下有什么不同:
1 | func testDerfer() { |
2)延迟函数执行按后进先出顺序执行,即先出现的defer最后执行
3)延迟函数可以操作主函数的具名返回值
1 | func testDerfer() (result int) { |
二、源码分析
可以先看一下_defer结构体字段,无特殊版本标记都是1.2版本出现的字段
1 | type _defer struct { |
通过看汇编代码,可以看到在1.14版本,会有如下三种情况处理defer
1 | func (s *state) stmt(n *Node) { |
在1.12版本使用的是堆分配,1.13版本加入栈分配,现在1.14版本又加入开放源码,可以看到堆分配是最后的兜底方案。现在我们会来介绍每一种方案。
1、堆分配(go 1.12)
声明defer关键字处使用**deferproc**() 注册defer处理函数,将对应的_defer结构体值拷贝到堆上。
对于新创建好的defer结构体会添加在defer链表的表头,goroutine的defer指针指向链表的表头,这也是为什么倒序执行defer的原因。
1 | //注册的函数类型是funcval类型,也就是defer的执行函数 |
没有捕获列表(没有外部函数使用的变量)的funcval(闭包) ,在编译阶段会进行优化 在只读数据段分配一个共用的funcval结构体。但当有捕获列表的funcval 在堆里分配funcval结构体的大小,捕获列表使用的变量在堆中会存储该变量,其他地方使用该变量则使用该变量的地址,所以值改变的时候引用的地方同时改变
主要流程是获取一个siz大小的_defer结构体,可以在协程对应的处理器的defer池获取,若获取不到,需要在堆上分配size大小的结构体,然后将该结构体放在链表头。
- 在ret指令返回时,插入**deferreturn()**,将defer链表的头取出执行。 执行时,会将值从堆上拷贝到栈上。
1 | func deferreturn(arg0 uintptr) { |
分析go1.12 defer的缺点
_defer结构体是堆分配,即使有预分配的defer池,也需要在堆上获取和释放,参数还要在堆栈间来回拷贝
使用链表注册defer信息,链表本身操作较慢
2、栈分配(go 1.13)
- 声明defer关键字处使用deferproc() 注册defer处理函数,把栈上的defer结构体注册到defer链表中。
1 | func deferprocStack(d *_defer) { |
- defer执行时,依然是通过deferreturn实现的,在defer函数执行时拷贝参数,不过不是在堆栈间,而是在栈上的局部变量空间拷贝到栈上的参数空间,性能提升30%
所以1.3的优化点:主要减少defer信息的堆分配。但是在显示和隐式的defer处理(例如for循环)还是使用defer1.2
3、开放编码(go 1.4)
1)开放编码的使用是有条件的,满足以下3个条件才会启用开放源码
1 | func walkstmt(n *Node) *Node { |
延迟比特和延迟记录是开放源码的主要结构:
1 | func buildssa(fn *Node, worker int) *ssa.Func { |
延迟比特中的每一个比特位都表示该位对应的 defer
关键字是否需要被执行。例如下面最后1位是1,说明接下来该位的defer会执行,使用或运算,把df的第一位置为1,标志该位可以执行,在执行的时候判断即可,判断完置为0,避免重复执行
在编译阶段插入代码,把defer的执行逻辑展开在所属函数内,从而免于创建defer结构体,去了构造defer链表项,并注册到链表的过程,不需要创建defer链表。
1 | func (s *state) stmt(n *Node) { |
openDeferRecord主要记录了defer的信息保存在openDeferInfo结构体中。
1 | type openDeferInfo struct { |
2)在延迟执行的时候,调用deferreturn函数进行执行。把defer需要的参数定义为局部变量,在函数返回前,直接调用defer的函数
1 | func deferreturn(arg0 uintptr) { |
在执行的时候先获取deferBits,然后判断哪个defer该进行执行
1 | func runOpenDeferFrame(gp *g, d *_defer) bool { |
1.14的缺点是:在某个defer函数执行过程中发生panic,后面的defer就不会被执行,就要去执行deferl链表了,但是这时open coded defer并没有注册到链表,这个时候需要额外通过栈扫描的方式来实现,所以panic变得慢了,但是panic发生的几率更小
这就是defer三种实现的原理