先来分享一个很多人踩过的坑:很多人以为defer是“函数返回后才执行”,但实际上它的执行时机更微妙——是在函数控制流离开当前栈帧之前。这个时间点,比你想象得要更靠后,搞清楚它对写出靠谱的Go代码至关重要。
defer 的参数在注册时就完成求值
这个是初学者最容易掉进去的陷阱之一。举个例子:
func example() { x := 10 defer fmt.Println("x =", x) // 此处 x 被拷贝为 10 x = 20 return}
输出一定是 x = 10,不是 20。原因很简单:defer 后面表达式中所有参数,在执行到该 defer 语句那一行时就已经完成计算并保存了。这个规则的影响面还挺广的:
- 值类型(
int、string、struct)会被完整拷贝 - 指针或引用类型(
*int、[]byte、map、chan)拷贝的是地址,后续修改会影响 defer 中读到的内容 - 闭包里捕获变量时,若用
func() { ... }形式不带参数,则访问的是变量最新值;但若显式传参如func(n int) { ... }(x),则仍是注册时的值
多个 defer 按 LIFO 顺序执行
不是按代码顺序,而是“最后声明的最先执行”。这个顺序决定了资源释放是否合理:
func openAndClose() { f1, _ := os.Open("a.txt") defer f1.Close() f2, _ := os.Open("b.txt") defer f2.Close() // 实际执行顺序:f2.Close() → f1.Close()}
- 适合嵌套资源释放(比如先关子连接、再关主连接)
- 不适合依赖顺序相反的场景(如锁嵌套),需手动调整注册顺序
- 循环中写
defer f(i)会全部注册同一份 i 的最终值,应改用defer func(n int) { f(n) }(i)
defer 在 return 之后、函数真正退出之前运行
这个“之间”阶段很微妙:返回值已写入栈帧,但函数还没退出。所以命名返回值可被 defer 修改:
func counter() (ret int) { ret = 1 defer func() { ret += 10 }() // 这里能改 ret return // 返回值 ret=1 已确定,但 defer 还没跑;defer 跑完才真正返回}
输出是 11。需要留意的是:
- 仅对命名返回值有效;
return 1这种匿名形式无法被 defer 修改 - 如果 defer 里 panic,会覆盖原始 return 值,触发 recover 流程
- panic 触发时,defer 仍会执行,且顺序仍是 LIFO
defer 性能开销在 Go 1.14+ 已基本可忽略
早期版本 defer 有明显栈分配和链表操作成本,但现在情况已大为改观:
- 绝大多数简单 defer(无闭包、参数少)走开放编码(open-coded),直接内联进函数末尾
- runtime.deferproc / deferreturn 调用几乎消失,反汇编看不到额外 CALL
- 只有含闭包或大参数的 defer 才回落到堆/栈分配路径
- 高频循环里每轮都 defer 仍不推荐,但普通业务函数里不用犹豫
真正要警惕的不是性能,而是逻辑错位:比如在 defer 里调用可能 panic 的函数,又没 recover,会导致 panic 被延迟传播,堆栈信息指向错误位置。