defer 的执行顺序是怎样的?它的参数何时求值?
结论先行
defer 在函数真正返回前按“后进先出(LIFO)”执行:先注册的后执行;且 defer 语句处的参数与接收者会立即求值并拷贝,只有闭包捕获的变量才在 defer 真正执行时才读取——这两条规则能解释绝大多数 defer 相关的输出题。
要点
- 执行时机:return 语句先把返回值求好,再运行 defer,最后才把控制权交回调用方,因此 defer 可以修改命名返回值。
- 执行顺序:多个 defer 逆序执行,类似栈;典型场景是资源成对打开后按相反顺序释放。
- 参数求值:defer f(x) 中的 x 在注册那一刻求值并拷贝;defer func() { use(x) }() 中 x 是捕获引用,执行时才取值。
- 可靠性保证:函数发生 panic、调用 runtime.Goexit 时,已注册的 defer 依然执行;但 os.Exit 不会执行 defer。
- 性能:Go 1.14 之后非循环中的 defer 采用开放编码(open-coded)优化,开销很小;循环体里海量注册 defer 仍应避免,可把循环体抽成函数。
- 实用习惯:defer 紧跟资源获取(锁、文件、连接),配合 defer mu.Unlock() 防止漏释放。
- 修改返回值:想用 defer 改返回值,函数必须使用命名返回值,匿名返回值在 return 时已被拷贝给调用方。
示例
func order() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(0) // 输出顺序:0 2 1
}
func result() (r int) {
defer func() { r += 10 }() // 修改命名返回值
return 1 // 实际返回 11
}
func argTiming() {
x := 1
defer fmt.Println(x) // 参数立即求值:打印 1
x = 2 // 不影响上面那行输出
}
常见追问/记忆点
- 追问:defer 里 recover 成功后,更早注册的 defer 还会执行吗?答:会,恢复后函数按正常路径返回,剩余 defer 照常逆序执行。
- 追问:为什么匿名返回值改不动?答:返回值在 defer 运行前就已拷贝到调用方,要改只能靠命名返回值。
- 追问:defer 能保证资源一定释放吗?答:只要进程没被 os.Exit/信号强杀,panic 与正常返回都会走 defer。
- 记忆:LIFO 逆序执行、参数当场求值、闭包变量跑前再取、命名返回值可被改。