defer

defer

defer 的历史

三大优势

  • 就近原则

资源清理动作(defer f.close())可以紧跟在资源获取动作之后(os.Open())

  • 函数级作用域

defer 始终是在函数返回前执行,所以无论有多少个if-else嵌套,也无论有多少个return语句,都不会影响 defer 的执行

  • 动态与条件执行

defer执行并非静态,通过结合if-else语句可以实现动态条件执行,可以根据上下文来决定是否要设置defer语句,更富有灵活性

双刃剑

由于defer关键字的动态性,即并非写多少个defer就要执行多少个defer,所以只能由 Go 运行时通过一个动态链表来维护defer调用栈。但这会带来极大的性能损耗。

原罪(≤Go 1.12/1.13)

在旧版本中,一个defer的执行过程如下:

  • 创建_defer结构体:每次执行到defer语句时,会在堆上分配一个_defer结构体,这个结构体中包含了函数指针、指向下一个_defer结构体的指针
  • 执行deferproc运行时调用:这个函数会将_defer结构体挂在到当前goroutine 的的链表上,即defer链表
  • 执行deferreturn运行时调用:在函数返回之前,编译器会插入对deferreturn的执行,该函数会倒序遍历defer链表上的每一个_defer结构体,并执行对应的函数

在旧版的流程中需要:

  • 一次堆内存分配(影响 GC、加锁)
  • 两次运行时调用(频繁在用户代码和运行时代码之间切换)

所以旧版本的实现效率较低

革命(Go 1.14)

新版本defer的核心思想是将非循环、简单的defer语句通过开发编码的方式进行调用,减少运行时函数调用以及频繁的内存拷贝。

一方面,在函数退出点直接生成代码:

新版本defer通过在栈上预留空间以及位掩码,替换了deferproc函数。在函数退出前,不再执行deferreturn函数,而是倒序遍历位掩码中的每一位,如果 bit 位为 1,那么就从栈上预留空间中取出函数指针和参数,发起函数调用。

另一方面,与 panic 流程深度整合:

在编译后的二进制文件中, 只要是使用了 open coded defer 机制的函数,都会有一份funcdata,它指明了三个信息

  • 该函数使用了 open coded defer
  • 位掩码在函数栈帧中的偏移量
  • 每个 defer的函数指针以及参数在函数栈帧中的偏移量

当发生 panic 时,gopanic函数就会扫描goroutine 栈,如果发现一个带有 open coded defer 的栈

小结与共识

image.png