Go 的 defer 在 return 赋值后、跳转前执行,panic 时也执行;参数在 defer 注册时求值,非执行时;返回值作用域和 LIFO 执行顺序易出错。

Go 的 defer 不是“函数末尾才跑”,而是嵌在函数退出流程里的固定环节:在 return 语句完成返回值赋值后、真正跳转前执行;panic 时也照常执行。但参数求值时机、返回值作用域、执行顺序这三点,几乎每个新手都会踩坑。
defer 参数在注册时就求值,不是调用时
这是最常误判的点。写 defer fmt.Println(i),打印的是这一行执行时的 i 值,不是函数退出时的值。
- 错误写法:
for i := 0; i → 全部输出 <code>3 - 正确写法(显式复制):
for i := 0; i - 或用闭包延迟读取:
for i := 0; i (注意传参,不是捕获变量) -
time.Now()、err.Error()这类表达式也立即求值——如果想记录 defer 实际执行时刻的时间,得写成defer func() { log.Println(time.Now()) }()
多个 defer 按 LIFO 顺序执行,不是代码顺序
它们被压入一个栈,函数退出时从栈顶弹出。这决定了清理逻辑必须倒着写,否则资源可能提前失效。
-
defer f.Close()和defer mu.Unlock()要写在mu.Lock()和f, _ := os.Open()之后,但执行时自动变成“先关文件、再解锁” - HTTP 客户端使用中:
defer resp.Body.Close()必须写在defer client.Close()之前,否则Body可能因连接关闭而读不到数据 - 若在循环里注册多个 defer(如批量打开文件),最终执行顺序是逆序迭代——容易导致 close 顺序错乱或 panic
命名返回值可被 defer 修改,匿名返回值不行
return 分两步:先赋值(命名返回值此时已存在),再触发 defer 链,最后返回。这个细节直接影响错误包装是否生效。
立即学习“go语言免费学习笔记(深入)”;
- 命名返回值:
func f() (err error) { defer func() { err = fmt.Errorf("wrap: %w", err) }(); return nil }→ 真的返回包装后的 error - 匿名返回值:
func f() error { e := errors.New("raw"); defer func() { e = fmt.Errorf("wrap: %w", e) }(); return e }→ 返回原始 error,defer改的是局部变量e - 指针返回值例外:
func() *int { i := 0; defer func(){i++}(); return &i }中i是栈变量,defer 确实改了它,但返回的指针可能悬空 - 多条 return 路径下,未显式赋值的命名返回值是零值,defer 修改它可能掩盖逻辑错误
nil 资源必须判空再 defer,recover 必须在 defer 内部调用
这两类错误不会报编译错,但运行时直接 panic 或静默失效,极难定位。
-
f, err := os.Open("x"); if err != nil { return err }; defer f.Close()是安全的;但漏掉if err != nil判断就defer f.Close(),f为nil会 panic -
sync.Mutex的Unlock()允许对零值调用,但自定义结构体若Unlock()含指针解引用,就得先判空 -
recover()只在 defer 函数体内调用才有效:defer func() { if r := recover(); r != nil { /* 处理 */ } }()✅;if r := recover(); r != nil { }写在 defer 外 ❌ - defer 函数里如果调用
os.Exit()或再次panic(),后续 defer 就被跳过——清理不完整
真正难的不是记住规则,而是每次写 defer 时都得问一句:这个参数现在求值,对吗?这个返回值是命名的吗?这个资源此刻一定非 nil 吗?


















