defer 是什么?

defer 修饰的函数是一个延迟函数,在包含它的函数返回时运行。

defer 执行时机

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking

defer 触发时机是:

  1. 函数执行到函数体末端
  2. 函数执行return语句
  3. 当前协程panic

defer 实现原理

​src/runtime/runtime2.go​

在这里插入图片描述

defer 执行机制

在中间代码生成阶段, 有三种不同的机制处理 defer 关键字

  • 堆上分配(Go 版本1.1-1.12)默认兜底方案
  • 栈分配(Go版本 1.13 )相比堆分配能够减少 30%堆额外开销。
  • 开放编码(Go 版本 1.14) 额外开销可以忽略不计。

堆上分配

​defer​​runtime.deferproc​​_defer​​Gorountine​​runtime.deferreturn​​Goroutine​​jmpdefer​
​runtime.deferproc​​runtime.deferreturn​

在这里插入图片描述

derfer 关键字最重要的三个函数

  1. deferproc。在每遇到一个defer关键字时,实际上都会转换为deferproc函数,deferproc函数的作用是将defer函数存入链表中。(go关键字是使用newproc函数,它两的实现有着不少相似之处)
  2. deferreturn。在return指令前调用,从链表中取出defer函数并执行。
  3. deferprocStack。go1.13后对defer做的优化,通过利用栈空间提高效率。

call 函数

​cmd/compile/internal/gc.state.call​
​cmd/compile/internal/gc.state.newValue1A​​defer​

deferproc 函数

​runtime.deferproc​​defer​​runtime._defer​​fn​​pc​​sp​
​runtime.funcval​​link​​runtime.return0​​runtime.deferreturn​

先通过 newdefer 获取一个 defer 关键字的插入顺序是从后向前的,而 defer 关键字执行是从前向后的,后调用的 defer 会优先执行。

newdefer追加新等延迟调用

deferreturn 函数

​runtime.deferreturn​​Goroutine​​_defer​
  • runtime.jmpdefer 是一个用汇编语言实现的运行时函数,它的主要工作是跳转到 defer 所在的代码段并在执行结束之后跳转回 runtime.deferreturn。
  • runtime.deferreturn 会多次判断当前 Goroutine 的 _defer 链表中是否有未执行的结构体,该函数只有在所有延迟函数都执行后才会返回
  • 最后调用的 runtime.return0 是唯一一个不会触发延迟调用的函数,它可以避免递归 runtime.deferreturn 的递归调用。

包含如下几个步骤:

  1. 判断执行条件
  2. 参数拷贝
  3. 释放_defer对象
  4. 执行函数

值得一提的是,defer具有即时传值的特点,defer也同样满足闭包和匿名函数的特性。

以代码为例子,讲解 defer 注册和执行流程

  1. 函数A定义局部变量a=1,b=2,存储在A函数的栈中
  2. deferproc函数注册defer函数A1时,
  • func deferproc(siz int32, fn *funcval)
  • siz:A1没有返回值,64位下一个整型参数占用8字节。
  • fn:A1函数入口地址,addr1

deferproc函数调用时,编译器会在它自己的两个参数后面,开辟一段空间,用于存放defer函数A1的返回值和参数。这一段空间会在注册defer时,直接拷贝到_defer结构体的后面。

在这里插入图片描述

  1. 在堆上分配存储空间,并存放_defer结构体
  • A1的参数加返回值共占8字节
  • defer函数尚未执行,所以started=false
  • sp就是调用者A的栈指针
  • pc就是deferproc函数的返回地址return addr
  • 被注册的function value为A1
  • defer结构体后面的8字节用来保存传递给A1的参数。然后这个_defer结构体就被添加到defer链表头,deferproc注册结束。

频繁的堆分配势必影响性能,所以Go语言会预分配不同规格的deferpool,执行时从空闲_defer中取一个出来用。没有空闲的或者没有大小合适的,再进行堆分配。用完以后,再放回空闲_defer池。这样可以避免频繁的堆分配与回收。

在这里插入图片描述

  1. deferreturn执行defer链表:从当前goroutine找到链表头上的这个_defer结构体,通过_defer.fn找到defer函数的funcval结构体,进而拿到函数A1的入口地址。接下来就可以调用A1了。调用A1时,会把_defer后面的参数与返回值整个拷贝到A1的调用者栈上。然后A1开始执行,输入参数值a=1。

在这里插入图片描述


欢迎关注公众号:程序员财富自由之路




参考资料