深入理解 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)。
现象背后的原因
先讨论表象背后的底层原理,即