Go 的 defer 是怎么实现的?为什么它会影响性能?
defer 常用,但它的实现方式在 Go 1.14 前后有本质变化:从"全部堆分配"变成"开放编码(open-coded)",
这也解释了为什么老代码里"defer 很慢"的结论现在不成立了。
1. 三种实现
| 版本/场景 | 实现 | 开销 |
|---|---|---|
| Go ≤1.13 | 堆分配 _defer 结构挂链表 | 每次 defer 一次分配 |
| Go 1.14+ | 开放编码:defer 少且无循环时内联到函数里 | 接近零成本 |
| 循环内/数量不定 | 退回堆分配(deferprocStack 栈上分配) | 仍有成本 |
开放编码的条件(编译器判断):
- defer 数量 ≤ 8;
- 不在循环里;
- 不是
defer可变数量(如defer f()在循环中)。
满足时,编译器在函数里直接展开成"正常路径 + 异常路径"两段代码,不需要建链表。
2. 执行顺序与返回值陷阱
func f() (n int) {
defer func() { n++ }() // 能改返回值:因为 n 是具名返回值
return 1 // 先给 n 赋值 1,再执行 defer → 返回 2
}
func g() int {
n := 0
defer func() { n++ }() // 改的是局部变量,返回值早已确定
return n // 返回 0
}
关键:return 不是原子操作,分两步——给返回值赋值 → 执行 defer。所以具名返回值能被 defer 改。
3. defer + recover 的位置
func safe() {
defer func() {
if r := recover(); r != nil { log.Println("recovered:", r) }
}()
panic("boom")
}
recover 只在 defer 中直接调用才生效;包一层函数(defer helper() 里再 recover)不行。
4. 什么时候仍然要避免 defer
- 超热路径(每秒百万次):即使是开放编码,也有额外指令与栈帧开销,benchmark 见真章;
- 循环里 defer:会退回堆分配,且直到函数返回才执行——常见的"文件句柄泄漏"就是这样来的:
for _, f := range files { defer f.Close() // 全部攒到函数结束才关,可能耗尽 fd } - defer 里做重活:它延后执行,容易掩盖耗时。
5. 一个 benchmark 结论
defer 无竞争、非循环、数量少 → 与直接调用差距在 1~3ns 级别(数量级相同)
循环内 defer / 老版本 → 每次多一次分配,差距可达 10 倍以上
所以实践建议:正常业务代码放心用 defer(释放锁、关文件、recover), 只有确认它是热点(pprof 里占比高)时再手动展开。
小结
defer 的性能问题基本是"历史印象":Go 1.14 的开放编码把常见场景的成本降到了接近零。 真正要注意的是语义——具名返回值会被 defer 改写、循环里 defer 会延迟到函数结束, 以及 recover 必须在 defer 中直接调用。