defer 参数在注册时立即求值,函数体延迟执行;循环中需显式捕获变量值,指针传参会读取最新值;defer 执行顺序为后进先出;高频使用有性能开销。

defer 后面的匿名函数参数在 defer 语句执行时立即求值
不是函数真正调用时,而是你写那行 defer func(x int) { ... }(v) 的那一刻,v 就被读取并拷贝进参数。哪怕 v 后续被修改,这个 defer 看到的仍是当时那个值。
常见错误现象:for i := 0; i < 3; i++ { defer fmt.Println(i) } 输出全是 3,因为 i 是同一个变量,循环结束时 i == 3,所有 defer 都在最后一轮才注册,此时 i 已是 3。
- 正确做法:在循环内显式捕获当前值,如
for i := 0; i < 3; i++ { i := i; defer fmt.Println(i) } - 或传参:
for i := 0; i < 3; i++ { defer func(x int) { fmt.Println(x) }(i) } - 注意:如果传的是指针(如
&i),那 defer 执行时读的是指针指向的最新值,行为完全不同
匿名函数体内的变量访问是延迟求值
函数体里的表达式(比如 t.q = t.q、fmt.Println(t.x))不参与参数求值,它们统统等到外层函数返回前才执行。这意味着你看到的变量值,是运行时那一刻的真实状态。
典型陷阱:想用 defer 恢复字段旧值,却写了 defer func() { t.q = t.q }() —— 这行代码注册时什么都没做,等真执行时 t.q 早已被改过,结果恢复的是新值,不是快照。
立即学习“go语言免费学习笔记(深入)”;
- 正确解法:把需要保存的值作为参数传入,利用参数的立即求值特性固化快照,如
defer func(q int) { t.q = q }(t.q) - 等效写法:先局部赋值,再闭包引用,如
qtmp := t.q; defer func() { t.q = qtmp }() - 命名返回值例外:它在 return 之后、defer 执行前已被赋值,所以 defer 中可直接读写该变量
defer 注册与执行顺序容易混淆
defer 语句出现的位置决定它何时注册(即参数求值),但执行顺序永远是后进先出(LIFO)。注册越晚,执行越早。
例如:defer fmt.Print("A") 写在第一行,defer fmt.Print("B") 写在第二行,最后输出是 B 在前、A 在后。
- 多个 defer 的参数求值顺序 = 代码书写顺序
- 多个 defer 的实际执行顺序 = 代码书写逆序
- 如果 defer 里再写 defer,嵌套的那个会在外层 defer 执行时才注册,因此更早触发
- panic 不影响 defer 执行时机,它们仍按 LIFO 在 panic 处理前运行
高频 defer 可能带来性能开销
每次 defer 都会分配一个 runtime._defer 结构体,并入栈管理。在 hot path(比如 tight loop)中大量使用,GC 压力和内存分配会明显上升。
常见场景:循环里每轮都 defer close(f) 或创建匿名函数,实际往往只需一次清理逻辑。
- 优先考虑提前释放资源,而非无脑 defer
- 循环中避免重复创建匿名函数;必要时提取为独立函数减少逃逸
- 确认 defer 是否真有必要——比如文件打开失败后根本没资源要关,就不该 defer Close
- 注意:Go 1.22+ 对循环中闭包的 defer 做了优化,但参数求值规则没变,不能依赖此优化绕过值捕获
最易被忽略的是:defer 不是对“整条语句”做快照,它只保证参数求值即时,其余一切(包括函数体内所有读写)都是延迟的。想靠 defer 自动记住变量历史状态,必须手动做参数传值或局部变量捕获。


















