前言
defer常用于锁的释放,连接关闭等场景,执行顺序是后进先出,靠后的defer先执行。那defer是怎么实现的呢?
实现思路
有两种思路:
- 由协程记录所有defer的执行代码,等return后调用运行。
- 在编译时插入defer的代码到函数末尾,然后直接运行函数即可。
记录代码后执行
首先,记录的代码可以分配在堆上,也可以分配到栈上。
分配在堆上的实现(go1.12之前方案):
- 在堆上开辟一个
shced.deferpool - 记录defer的语句,加入到
deferpool中 - 函数结束后执行
deferpool中的语句
Go用_defer结构体用于抽象defer信息。_defer结构体如下:
type _defer struct {
started bool
heap bool // 是否为堆分配方式
openDefer bool // 是否为开放式编码
sp uintptr // 调用函数的栈地址
pc uintptr // 返回地址
fn func() // 注册的函数
_panic *_panic
link *_defer // 链表,找到下一个_defer执行
fd unsafe.Pointer
varp uintptr
framepc uintptr
}
当函数结束后,会调用deferreturn方法,里面循环的执行defer语句。源码如下:
func deferreturn() {
gp := getg() // 获取当前运行的协程g
for {
d := gp._defer // 获取defer的信息
if d == nil {
return
}
sp := getcallersp()
if d.sp != sp {
return
}
// 为开放式编码,编译时会将defer代码直接插入到函数尾部,
// 不会在这里执行,因此直接return
if d.openDefer {
done := runOpenDeferFrame(d)
if !done {
throw("unfinished open-coded defers in deferreturn")
}
gp._defer = d.link
freedefer(d)
// If this frame uses open defers, then this
// must be the only defer record for the
// frame, so we can just return.
return
}
fn := d.fn
d.fn = nil
gp._defer = d.link // 准备下一个defer信息
freedefer(d) // 从deferpool中释放该defer
fn() // 执行defer语句
}
deferreturn中,使用for循环一直执行defer链表,直至完成。
当然,将defer信息记录到堆上,就会有指针逃逸,增加堆内存。同时需要来回拷贝defer信息,因此执行速度较慢。
于是Go1.13中做了优化,将defer信息放到函数栈上存储,实现方法也和堆分配差不多,都是先记录defer信息,再回调执行。当然,栈分配会出现栈空间不足的情况。
编译时插入代码
Go1.14中对defer进一步改进,对于简单的代码片段,会在编译时插入到函数末尾,直接省去了变量的拷贝和记录defer代码过程,效率更高。
但也有个缺点,正常情况下若是某个defer出现panic了,剩余defer应该得继续执行。而插入代码的方式由于panic,函数直接退出,就无法实现这一需求,因此还得回到协程_defer字段中扫描出剩余的defer代码,并执行。这时候效率就低了。
当然,一般情况下panic次数较少,这种优化还是很不错的。
总结
Go的defer有两种思路,三种实现方法。目前是三种方法混合使用。毕竟对于以下代码片段还是得用堆分配来实现。
for i := 0; i < 20; i++ {
defer func(i int) {
i++
}(i)
}
over!