讲解

defer 把一条函数调用「登记」到当前函数返回前执行,常用于资源清理:打开文件后立刻 defer f.Close(),无论后面多少分支、中途哪里 return,关闭动作都不会漏。多个 defer 按「后进先出」的栈序执行——先登记的最后执行,所以依赖顺序的清理要注意登记次序。defer 让「申请」和「释放」写在相邻两行,是对抗资源泄漏最优雅的语言机制之一。

panic 是程序遇到无法继续的严重错误时的「紧急制动」:运行时 panic(数组越界、nil 解引用)或主动调用 panic(值)。panic 会沿调用栈向上传播,沿途执行各层的 defer,直到程序崩溃退出。Go 的哲学是:可预期的错误用 error 返回值,panic 只留给真正的程序 bug 和不可恢复的局面——它不是异常处理的替代品。

recover 是 panic 的「安全网」:在 defer 的函数里调用 recover() 能捕获正在传播的 panic,让程序恢复执行。它只在 defer 里有效,典型用法是在服务边界(如 HTTP 请求处理的最外层)兜底,防止单个请求的 bug 拖垮整个进程。日常代码里 recover 应该极少出现——如果你经常想用它,多半是该用 error 的地方用了 panic。

示例

defer 的后进先出执行顺序:

package main

import "fmt"

func main() {
	defer fmt.Println("最后执行(defer 1)")
	defer fmt.Println("倒数第二(defer 2)")
	fmt.Println("正常逻辑先执行")

	for i := 0; i < 3; i++ {
		defer fmt.Println("循环内 defer:", i) // 输出 2、1、0,逆序弹出
	}
}

defer 做资源清理:文件句柄与临时文件:

package main

import (
	"fmt"
	"os"
)

func main() {
	f, err := os.CreateTemp("", "demo-*.txt")
	if err != nil {
		fmt.Println("创建失败:", err)
		return
	}
	defer os.Remove(f.Name()) // 无论后面怎样都会清理
	defer f.Close()           // 后登记先执行:先 Close 再 Remove

	f.WriteString("临时数据\n")
	fmt.Println("临时文件:", f.Name())
}

在 defer 里 recover 兜住 panic:

package main

import "fmt"

func safeDivide(a, b int) (result int) {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("从 panic 中恢复:", r)
			result = -1 // 通过命名返回值给出兜底结果
		}
	}()
	return a / b
}

func main() {
	fmt.Println(safeDivide(10, 2))
	fmt.Println(safeDivide(1, 0)) // 除零 panic 被兜住
	fmt.Println("程序没有崩溃,继续执行")
}

常见坑

  • 把 defer 当finally 用在循环里堆资源:for 循环里每次迭代 defer 一个 Close,所有清理都憋到函数返回才执行,句柄可能先耗尽。循环内需要及时释放的,把迭代体抽成小函数或显式 Close。
  • recover 写在普通函数里:recover 只有在 defer 直接或间接调用的函数里才有用,其他地方调用一律返回 nil。
  • 用 panic 做业务错误:「用户不存在」是可预期错误,该返回 error。panic 留给数组越界、断言失败这类「代码有 bug」的信号。
  • defer 的参数求值时机误解:defer fmt.Println(x) 的参数 x 在 defer 登记时就求值定死了,后面改 x 不影响输出;想捕获变化要包一层闭包 defer func() { fmt.Println(x) }()。
  • recover 后假装没事:兜住 panic 至少要把错误和堆栈打进日志,静默吞掉等于把 bug 埋进黑盒,排查时痛不欲生。

小结

defer 登记收尾、后进先出;panic 是紧急制动,recover 在 defer 里兜底;业务错误走 error,panic 留给 bug。下一节学习组织代码:包与模块。