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 的栈
小结与共识
