defer
基础知识
延迟的函数的什么时候被调用?
- 函数 return 的时候
- 发生 panic 的时候
延迟调用的语法规则
- defer 关键字后面表达式必须是函数或者方法调用
- 延迟内容不能被括号括起来
defer 用途
- 资源的释放
- 配合 recover 一起处理 panic
使用形式
defer 在使用的时候,只需要在其后面加上具体的函数调用即可,这样就会注册一个延迟执行的函数 func,并且会把函数名和参数都确定,等到从当前函数退出的时候在执行。
defer 的底层结构
进行 defer 函数调用的时候其实会生成一个_defer 结构,一个函数中可能有多次 defer 调用,所以会生成多个这样的_defer 结构,这些_defer 结构链式存储构成一个_defer 链表,当前 goroutine 的_defer 指向这个链表的头节点,_defer 的结构定义在 src/src/runtime/runtime2.go 中。
defer 的执行过程
defer 关键字的处理在生成 SSA 中间代码阶段,编译器遇到 defer 语句的收,会插入两种函数:
- defer 内存分配函数:deferproc(堆分配) 或 deferprocStack(栈分配)
- 执行函数:deferreturn
defer 的是现有三种实现方式,在栈上分配内存,在堆上分配内存以及使用开放编码的方式。会优先使用内联方式,当内联不满足,且没有发生内存逃逸的情况下,使用栈分配的方式,这两种情况都不符合的情况下在使用对分配,这样做的好处是提升性能。
_defer 内存分配
堆上分配
go 1.13 之前只有这个函数,说明 go 1.13 之前,_defer 只能在堆上分配。
堆上 defer 的创建思想借助了内存复用,用到了内存池的思想,创建 defer 的过程是:优先在 p 的本地和全局的 defer 缓存池里找到一个可用的 defer 结构返回,找不到在去堆上创建。
栈上分配
在栈上分配_defer,这个函数是 go 1.13 之后引入的,优化 defer 性能的,显然在栈上分配的效率更高。
开放编码
这种方式是在 go1.14 引入的继续优化 defer 实现性能的方式。在 go1.14 中通过代码内联优化,使得函数末尾直接对 defer 函数进行调用,减少了函数调用开销。
defer 在 for 循环中调用,编译器不确定会执行多少次,会逃逸到堆上,这样 defer 就只能分配在堆中了。所以在使用 defer 延迟调用的时候,尽量不要在循环中使用,否则可能导致性能问题。
总结一下:在 g1.14之后,go 会优先采用内联的方式处理 defer 函数调用,但是需要满足以下几个条件:
- build编译的时候没有设置-N
- defer 函数个数没有超过 8 个
- defer所在函数返回值个数和defer函数个数乘积不超过15
- defer没有出现在循环语句中时
defer 函数执行
当 go 函数的 return 关键字执行的时候,触发 call 调用 deferreturn 函数,deferreturn 函数的执行逻辑也很简单,就是遍历 goroutine 上的 defer 链表,从表头开始遍历,依次取出 defer 结构执行 defer 结构中的函数执行。