defer 原理¶
约 730 个字 2 行代码 1 张图片 预计阅读时间 4 分钟
defer 是什么¶
defer 用于将**函数调用**推迟到**当前函数返回**(或发生未恢复 panic、即将沿栈展开)时再执行。一个函数里可以有多条 defer,执行顺序为**后进先出(LIFO)**。
使用形式¶
要点:defer 后面的**参数表达式**会在遇到 defer 语句时立即求值,只有**被调用的函数体**延后到返回阶段执行。
底层结构:_defer¶
每次 defer 会在当前 goroutine 上挂接一个 _defer 节点;多个 _defer 组成**链表**,新节点插在表头,因此执行时从表头依次弹出,表现为 LIFO。
结构体(字段随版本可能微调,见 runtime/runtime2.go)核心含义:
sp/pc/fn:记录调用点与要执行的函数。link:指向同一 goroutine 上**更早**注册的_defer,形成链表。heap:是否为堆上分配的_defer(影响回收方式)。openDefer:是否采用**开放编码**(见下文)。
函数返回路径上,运行时会调用 deferreturn,按当前栈帧匹配 _defer 并执行。
编译与分配策略¶
编译器在 SSA 阶段处理 defer,大致三类实现(由编译器择优):
-
开放编码(open-coded,约 Go 1.14+)
在满足条件时,把 defer 逻辑展开在函数出口附近,减少deferproc调用开销。常见限制包括:defer个数不宜过多、与返回值/defer数量组合不能过大、不在循环中动态增加难以静态分析的defer等。 -
栈上分配:
deferprocStack
未逃逸、可静态分析时,在栈上放置_defer记录,再链入 goroutine 的 defer 链表。 -
堆上分配:
deferproc/newdefer
早期版本或无法满足栈/开放编码条件时使用;newdefer会尽量从 P 本地 defer 池 与**全局 defer 池**取复用对象,减少频繁堆分配。
经验:在**循环里**反复 defer 会导致大量 _defer 分配或无法开放编码,容易带来性能问题;应改为在循环外 defer 一次,或显式使用函数封装缩小 defer 次数。
执行:deferreturn¶
返回阶段,deferreturn 会遍历当前 goroutine 的 defer 链表,只处理**仍属于当前栈帧**的节点(通过 sp 等判断),依次执行 fn 并释放 _defer。若使用开放编码,则走 runOpenDeferFrame 等分支。
小结¶
defer注册顺序与执行顺序相反(LIFO)。- 参数在 defer 语句处求值,勿误以为在函数最后一刻才计算。
- 实现上分堆分配、栈分配与开放编码,目标是在保证语义的前提下降低开销。
- 避免在热路径循环内无节制使用
defer。