defer 原理¶
约 731 个字 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。