defer

基础知识

延迟的函数的什么时候被调用?

延迟调用的语法规则

defer 用途

使用形式

defer 在使用的时候,只需要在其后面加上具体的函数调用即可,这样就会注册一个延迟执行的函数 func,并且会把函数名和参数都确定,等到从当前函数退出的时候在执行。

defer 的底层结构

进行 defer 函数调用的时候其实会生成一个_defer 结构,一个函数中可能有多次 defer 调用,所以会生成多个这样的_defer 结构,这些_defer 结构链式存储构成一个_defer 链表,当前 goroutine 的_defer 指向这个链表的头节点,​_defer 的结构定义在 src/src/runtime/runtime2.go 中。

defer 的执行过程

defer 关键字的处理在生成 SSA 中间代码阶段,编译器遇到 defer 语句的收,会插入两种函数:​

  1. defer 内存分配函数:deferproc(堆分配) 或 deferprocStack(栈分配) ​
  2. 执行函数: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 函数调用,但是需要满足以下几个条件:​

defer 函数执行

当 go 函数的 return 关键字执行的时候,触发 call 调用 deferreturn 函数,deferreturn 函数的执行逻辑也很简单,就是遍历 goroutine 上的 defer 链表,从表头开始遍历,依次取出 defer 结构执行 defer 结构中的函数执行。