defer 在函数控制流离开当前栈帧前执行,参数注册时求值,LIFO 执行顺序,可修改命名返回值,Go 1.14+ 性能已优化。

defer 不是“函数返回后再执行”,而是“在函数控制流离开当前栈帧前执行”——这个时机点比很多人想的更靠后,也更关键。
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 修改:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
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 被延迟传播,堆栈信息指向错误位置。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










