Go defer的底层原理是怎样的?

121 阅读3分钟

前言

defer常用于锁的释放,连接关闭等场景,执行顺序是后进先出,靠后的defer先执行。那defer是怎么实现的呢?

实现思路

有两种思路:

  1. 由协程记录所有defer的执行代码,等return后调用运行。
  2. 在编译时插入defer的代码到函数末尾,然后直接运行函数即可。

记录代码后执行

首先,记录的代码可以分配在堆上,也可以分配到栈上。

分配在堆上的实现(go1.12之前方案):

  1. 在堆上开辟一个shced.deferpool
  2. 记录defer的语句,加入到deferpool
  3. 函数结束后执行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!