深入理解 Go defer 版本差异与底层特性
defer 简介
defer 的作用是使某个/某些执行延时执行,常用在资源使用后的释放等场景,例如:
func DbInit() (sql.DB, error) {
    db, err := sql.Open()
    if err != nil {
        return nil, err
    }
    defer db.Close()
    ...
}
defer 有一些熟知的特性:
- “先进后出”,也就是说如果使用了多个 defer,最先使用 的 defer 后的函数体 最后执行。
 - 一定会执行,即 panic 后依然有效。
 - 执行体参数在注册 defer 时确定。
 - …
 
我们先来看看为什么。
结构定义
先来看看标准库中 runtime/runtime2.defer 的定义。
type _defer struct {
	siz     int32	// 函数参数和返回值的大小
	started bool
	heap    bool	// 是否分配在堆上
	openDefer bool	// 是否经过开放编码的优化
	sp        uintptr  // defer函数体的栈顶指针
	pc        uintptr  // 下一条指令地址
	fn        *funcval // 执行函数地址,如果是开放编码可以为空
	_panic    *_panic  // 与之相关的panic链表
	link      *_defer	// defer链表
	...
}
除堆分配和开放编码相关字段之外,其余字段很容易理解。这两个特性是在 1.13 与 1.14 引入的,重点解决 defer 性能问题,放在下文。还有一些字段是关于 gc 的,理解较为复杂,暂不讨论。
除此之外,有两个字段比较重要,link 与 _panic。前者将注册的许多 defer 串成链表,在执行的时候按预先定义的顺序访问,先恢复执行体函数所需的栈信息等操作,跳转到函数地址处执行;后者将 defer 与 panic 进行关联,确保发生 panic 后按序执行所有注册过的 defer 函数。
编译阶段会把 defer 关键字分为三种状态,分别是开放编码,栈分配,堆分配。
func (s *state) stmt(n *Node) {
    ...
    switch n.Op {
        ...
        case ODEFER:
			...
			if s.hasOpenDefers {
				s.openDeferRecord(n.Left)	// 开放编码
			} else {
				d := callDefer	// 堆分配
				if n.Esc == EscNever {
					d = callDeferStack	// 栈分配
				}
				s.callResult(n.Left, d)
			}
        ...
    }
    ...
}
早起的 Go (1.13 之前)会把 defer 所需内存全部分配在堆上,性能较差,在 1.13 时通过调用分析可选择分配在栈上以增加性能,提升了大约 30%,1.14 又增加了开放编码方式,使 defer 的调用损耗可以忽略不计(<10ms)。
现象背后的原因
先讨论表象背后的底层原理,即
版本差异
总结
