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 逆序执行、参数当场求值、闭包变量跑前再取、命名返回值可被改。
笔记加载中…