defer本质是注册延迟任务,参数当场求值、执行时栈逆序触发;defer语句中参数是值快照而非引用,后续修改变量不影响已注册的defer执行结果。

defer 不是“等函数结束再调用”,而是“注册一个延迟任务,参数当场求值,执行时按栈逆序触发”——这个本质没搞清,后面所有坑都会反复踩。
defer 参数在注册时就求值,不是执行时
写 defer fmt.Println(i),打印的是这一行执行时的 i 值;后续改 i 不影响它。这和闭包里捕获变量地址不同:前者是值快照,后者是引用。
- 错误写法:
for i := 0; i → 全输出 <code>3 - 正确写法(传参快照):
for i := 0; i - 正确写法(局部绑定):
for i := 0; i - 想记录真实执行时间?不能写
defer log.Println(time.Now()),得写defer func() { log.Println(time.Now()) }()
多个 defer 按 LIFO 顺序执行,顺序错资源就漏关
越晚写的 defer 越早执行。这不是代码阅读顺序,而是注册入栈顺序——直接影响资源释放安全。
-
resp, err := client.Do(req)后,必须先defer resp.Body.Close(),再defer client.Close();否则连接可能提前断开,Body读不到完整数据 - 批量打开文件时,
for _, f := range files { f, _ := os.Open(...); defer f.Close() }会导致 close 逆序执行,若某次Close()panic,后面文件就永远不关 - 锁嵌套场景:
mu1.Lock(); defer mu1.Unlock()和mu2.Lock(); defer mu2.Unlock()必须按加锁反序注册,否则可能死锁
命名返回值能被 defer 修改,匿名返回值不能
只有命名返回值(如 func f() (err error))在函数栈帧中有固定地址,defer 才能真正改掉最终返回值。
- 有效:
func f() (err error) { err = io.ErrUnexpectedEOF; defer func() { err = fmt.Errorf("wrap: %w", err) }(); return }→ 返回包装后的 error - 无效:
func f() error { e := io.ErrUnexpectedEOF; defer func() { e = fmt.Errorf("wrap: %w", e) }(); return e }→ 返回原始 error,e是局部变量 - 陷阱:
func() *int { i := 0; defer func(){i++}(); return &i }→ 返回悬空指针,i栈空间已回收
recover 必须在 defer 函数体内调用才生效
recover() 单独写在外面或 defer recover() 都无效——前者没 panic,后者是注册时就执行了 recover 并返回 nil。
- 正确写法:
defer func() { if r := recover(); r != nil { log.Printf("panic caught: %v", r) } }() - goroutine 独立:主 goroutine 的
defer捕不到子 goroutine 的 panic,每个协程需自己配defer+recover - nil 指针陷阱:
obj := NewResource(); defer obj.Close(),若NewResource()返回nil,obj.Close()运行时 panic;应写成defer func(){ if obj != nil { obj.Close() } }()
最常被忽略的点:defer 不是语法糖,它是 runtime 层级的栈链表操作;高频路径(比如每请求都 defer 日志、defer 错误处理)会带来可观开销,这时候该手动 cleanup 就手动 cleanup。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











