defer 是栈式收尾协议 — LIFO 逆序, 参数立即求值; panic 沿调用栈逐帧展开, 只能被 defer 里的 recover 截住
defer 是把"收尾动作"压栈: 函数 return 时按后进先出逆序弹出来执行。参数在 defer 语句执行那一刻立即求值定格, 闭包则引用变量的最终值, 命名返回值还能被改写 —— 三条规则决定了所有惯用法的形状。
panic 是沿调用栈向上的"紧急撤离": 每一帧已注册的 defer 仍会执行, 唯一的截停点是 defer 里直接调用的 recover()。救不回来就进程退出 exit 2 —— 所以每条 goroutine 都要有自己的 recover 门卫, 这是服务不死的底线。
defer fmt.Println("A") // 先注册: 资源 A defer fmt.Println("B") // 后注册: 资源 B // 函数返回时输出: // → B 关键: 后注册的先执行 // → A 与打开顺序互为镜像
defer log("id=%d", id) 的 id 在 defer 语句执行处定格; defer func(){ log(id) }() 拿到的才是闭包引用的最终值。要快照传参数, 要直播用闭包。i := 7 defer fmt.Println(i) // 定格: → 7 defer func() { fmt.Println(i) }() // 直播: → 42 (先弹出) i = 42 // 关键: 传参是快照, 闭包是直播
func f() (err error) 的 defer 里对 err 赋值会改写真正的返回值 —— 统一错误包装/补指标的标准挂点。func f() (err error) { defer func() { // 关键: 改的就是返回值槽 err = fmt.Errorf("wrap: %w", err) }() return doWork() }
func f() (n int) { defer func() { n++ }() return 5 // 三步: n=5 → defer n=6 → RET } fmt.Println(f()) // → 6
defer func() { // 对: 直接函数体里调用 if r := recover(); r != nil { handle(r) } }() // 错: defer tryRecover() — helper 里调 recover 永远 nil
func Load(path string) (*Cfg, error) { // 对: 返回 error return nil, fmt.Errorf("open %s", path) } // panic 只留给 bug 现场: nil map 写入 // var m map[string]int; m["k"]=1 → panic: assignment to entry in nil map
panic(r) 原值再抛, 并用 debug.Stack() 当场留栈 —— recover 之后原栈就开始被收走了。defer func() { if r := recover(); r != nil { stack := debug.Stack() // 关键: 此刻拍照, 栈即将被收走 panic(r) // 原值再抛, 不吞现场 } }()
func f() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("panic: %v", r) // 转错误返回 } }() panic("boom") // → err="panic: boom" }
go 出去的入口必须统一套 recover 壳。go func() { panic("boom") // 错: 整进程 exit 2 }() go recoverWrap(consume) // 对: 统一出口自带 recover 壳
// net/http 自带 recover: 只记日志 + 断连接 // 客户端收到 connection reset, 不是 500 JSON defer func() { // 对: 中间件里自己兜 if r := recover(); r != nil { w.WriteHeader(http.StatusInternalServerError) } }()
func f() { defer cleanup() // 1.14+: 折成 1ns 位操作, 无堆分配 work() } // 只有循环内 defer / 单函数超 8 个才退化堆 defer 链
os.Exit/log.Fatal 直接走 syscall 退出, 已注册 defer 一个都不跑; main 里要 return err 到统一出口再 Exit。func main() { defer log.Sync() // 错: os.Exit 后永不执行 if err := run(); err != nil { os.Exit(1) } } // 对: run() 收口函数里 defer flush 完成后再 Exit
panic(nil) 让 recover 返回 nil 无法判别; 1.21+ 默认转成 *runtime.PanicNilError (GODEBUG=panicnil=1 恢复旧行为)。永远 panic 非 nil 值最稳。panic(nil) // 1.21 前: recover() → nil, 判不出发生过 panic // 1.21+: recover() → *runtime.PanicNilError // 关键: 永远只 panic 非 nil 值最稳
for _, p := range paths { f, _ := os.Open(p) defer f.Close() // 错: 堆到函数结束, fd 泄漏 } // 对: 循环体抽成函数, defer 立即生效
一个 handler 里的 nil 指针 panic 会把整条连接打断, 监控只看到 connection reset; 统一兜住才有 500 可查:
func recoverMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { // 只能在这里直接调 log.Printf("panic: %v\n%s", rec, debug.Stack()) if r.Context().Err() != nil { return // 客户端已断开: 补写响应无意义 } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(errResp{Code: "internal_error"}) } }() next.ServeHTTP(w, r) // 业务 handler 的 panic 全被兜住 }() }
无脑 defer f.Close() 会吞掉 Close 的错误 —— 页缓存没刷完就是静默丢数据:
f, err := os.Create(path) if err != nil { return err // ① 失败就别注册 defer (nil f 会 panic) } defer func() { // ③ 收尾: Close 的错误不能丢 if cerr := f.Close(); cerr != nil { err = errors.Join(err, fmt.Errorf("close %s: %w", path, cerr)) } }() if _, werr := f.Write(buf); werr != nil { // ② 主逻辑 return werr } return f.Sync() // Close 不保证刷盘, 账单类数据要 Sync
手写 Unlock 的函数每加一个 return 分支就多一个死锁隐患; defer 一注册全兜住:
func (c *Cache) GetOrLoad(k string) (Item, error) { c.mu.RLock() hit, ok := c.m[k] // 读锁内只做快速路径 c.mu.RUnlock() if ok { return hit, nil } c.mu.Lock() // 慢路径升级写锁 defer c.mu.Unlock() // 之后 4 个 return 分支都不用记得解锁 if hit, ok := c.m[k]; ok { // double-check: 等锁期间可能已被填上 return hit, nil } v, err := load(k) if err != nil { return Item{}, err // 提前返回, defer 保证解锁 } c.m[k] = v return v, nil }
五处 return 就要写五次包装和打点; 挂在 defer 里一处收口:
func (s *Service) Call(ctx context.Context, req *Req) (resp *Resp, err error) { started := time.Now() defer func() { // 命名返回值: 这里改的 err 就是返回值 cost := time.Since(started) if err != nil { err = fmt.Errorf("service=%s req_id=%s cost=%s: %w", s.name, req.ID, cost, err) // %w 包装: 上层 errors.Is 仍可用 s.metrics.ErrTotal.WithLabelValues(s.name).Inc() return } s.metrics.CostMS.WithLabelValues(s.name).Observe(float64(cost.Milliseconds())) }() resp, err = s.doCall(ctx, req) // 注意用 = 不是 :=, 否则遮蔽返回值 return }
SQL 事务三步写完, 中间任何 return 都要回滚; defer 让"忘记回滚"从代码里消失:
func (s *Store) CreateOrder(ctx context.Context, o Order) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() // commit 成功后 Rollback 返回 ErrTxDone, 无害 if _, err := tx.ExecContext(ctx, insertOrderSQL, o.ID, o.Amount); err != nil { return err // 这里返回 → defer 回滚 } if _, err := tx.ExecContext(ctx, deductStockSQL, o.SKU, o.Qty); err != nil { return err // 同上: 任何失败路径统一被兜 } if _, err := tx.ExecContext(ctx, insertLedgerSQL, o.ID, o.Amount); err != nil { return err } return tx.Commit() // 走到这里才提交; 没提交的都回滚 }
导出循环里 defer f.Close(), 函数不返回 defer 不执行 —— fd 一路涨到 ulimit:
// 事故: exportAll 循环 50 万个文件, 每个都 defer f.Close() // 症状: lsof 一路涨, 半小时后 "too many open files", 进程卡死 func exportAll(paths []string) error { for _, p := range paths { if err := exportOne(p); err != nil { // 修复: 每文件一个函数作用域 return err } } return nil } func exportOne(path string) error { f, err := os.Open(path) if err != nil { return err } defer f.Close() // 函数结束立即执行, 不再堆积 return streamTo(f) }
修复后 lsof 稳定在个位数; 数据量小也可在循环体内显式 Close, 但函数封装最不易漏。
几十层递归里逐层 if err 判空太啰嗦; 哨兵 panic + recover 转错误是标准解法 (stdlib template/JSON 解码同款):
var errAbortWalk = errors.New("walk aborted") func walkTree(n *Node, visit func(*Node) error) (err error) { defer func() { if r := recover(); r != nil { if r == errAbortWalk { // 哨兵 panic: 最快退出通道 err = errAbortWalk // recover 转成正常错误返回 return } panic(r) // 别的 panic 原样再抛: 不吞现场 } }() eachNode(n, visit) return nil } // 调用方: visit 返回 errAbortWalk 时内部直接 panic, 免掉逐层判空
裸 go func() 起的后台任务一旦 panic 就是全进程 exit 2, 半夜被告警叫醒; 统一出口拦住并上报:
func Go(name string, fn func()) { // 项目统一的 go 出口 go func() { defer func() { if r := recover(); r != nil { stack := debug.Stack() // 此刻不拍, 栈就被展开收走了 sentry.CaptureException(fmt.Errorf("goroutine %s panic: %v\n%s", name, r, stack)) log.Error("goroutine panic", "name", name, "panic", r) } }() fn() }() } // 用法: Go("order-consumer", consume) — 子任务崩了只上报, 不拖死整个进程
启动顺序 listener → DB → MQ, 停机必须逆序: 先停流量再关依赖, defer 的 LIFO 天然就是这张图:
func run(ctx context.Context) error { lis, err := net.Listen("tcp", ":8080") if err != nil { return err } defer lis.Close() // 后开的最先关: 与启动顺序互为镜像 db, err := sql.Open("mysql", dsn) if err != nil { return err } defer db.Close() // 连接池在 listener 之后收 sub, err := mq.Subscribe(ctx, topic, handler) if err != nil { return err } defer sub.Unsubscribe(ctx) // 先停流量, 再关依赖 return http.Serve(lis, makeMux()) // ctx 取消 → Serve 返回 → defer 逆序收尾 }
流式下载写了一半 panic, 客户端早已超时断开 —— 这时补 500 既不可能也没人收:
defer func() { if rec := recover(); rec != nil { log.Printf("panic: %v\n%s", rec, debug.Stack()) if r.Context().Err() != nil { return // 499: 客户端已断开, 写了也没人收 } if w.Header().Get("Content-Length") == "" && !written(w) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) // 只有没写过 header 才能补 500 json.NewEncoder(w).Encode(errResp{Code: "internal"}) return } // 已写一半 (流式下载中断): 只能记日志, 连接交给 server 断 log.Error("partial response abandoned", "path", r.URL.Path) } }() // written(w): 中间件包一层响应记录器, 记录 WriteHeader/Write 是否已被调用
for _, p := range paths { f, _ := os.Open(p) defer f.Close() // 错: 堆到函数结束 → too many open files } // 对: 循环体抽成 exportOne(p), defer 立即生效
defer fmt.Println(i) 打印注册时的 i, 闭包版打印最终值, 输出对不上预期. 原因: 参数当场定格. 正解: 要快照直接传参, 要最终值用 defer func(){ fmt.Println(i) }()。i := 7 defer fmt.Println(i) // 错: 想要 42, 打的却是 7 defer func() { fmt.Println(i) }() // 对: 闭包拿最终值 42 i = 42
func tryRecover(){ recover() }, panic 照炸. 原因: recover 只在 defer 的直接函数体里有效. 正解: defer func(){ if r := recover(); ... }() 原地写。func tryRecover() { recover() } defer tryRecover() // 错: 永远拦不住, panic 照炸 defer func() { // 对: 直接函数体里调 if r := recover(); r != nil { handle(r) } }()
if r := recover(); r != nil { continueJob(m) // 错: map 写了一半, 二次 panic err = toErr(r) // 对: 只收尾转错误, 状态重建 }
go 出去的入口统一套 recover 壳并上报。go consume() // 错: panic → 整进程 exit 2 func Go(fn func()) { // 对: 统一出口自带 recover 壳 go func() { defer func() { if r := recover(); r != nil { report(r) } }() fn() }() }
cerr := f.Close(), 用 errors.Join 并入返回值, 关键数据再加 f.Sync()。defer f.Close() // 错: Close 失败被吞, 数据丢 defer func() { // 对: 错误并进返回值 if cerr := f.Close(); cerr != nil { err = errors.Join(err, cerr) } }()
func f() error 里 defer 对局部 err 赋值, 调用方拿到的还是旧值. 正解: 声明命名返回值 (err error), defer 才改得到返回值槽。func f() error { // 错: 非命名返回值 err := do() defer func() { err = wrap(err) }() // 只改了局部副本 return err // 调用方拿旧值 } func f() (err error) { ... } // 对: 命名返回值
panic(errors.New("x")) 后 r.(MyErr) 断不上, 因为动态类型是 *errors.errorString. 正解: 断言与 panic 用同一定义的自定义类型, 或直接与哨兵值 == 比较。panic(errors.New("x")) // 错: 动态类型是 *errors.errorString _, ok := recover().(MyErr) // → ok=false, 断不上 panic(&MyErr{}) // 对: 同一定义的类型 if e, ok := recover().(*MyErr); ok { handle(e) }
func init() { cfg = mustLoad() // 错: panic → CrashLoopBackOff registerMetrics() // 对: init 只做注册类无失败操作 } // 对: 能失败的下沉到 main 里 return err
defer s.mu.Unlock() 解的是副本锁, go vet 报 copylocks. 正解: 含锁 struct 一律指针传递; 变量可能重绑定时用 defer func(){ mu.Unlock() }()。func (s S) Do() { // 错: 值拷贝, go vet: copylocks s.mu.Lock() defer s.mu.Unlock() // 解的是副本锁 } func (s *S) Do() { ... } // 对: 含锁 struct 一律指针传递
debug.Stack() 当场拍照再处理。if r := recover(); r != nil { log.Error(toErr(r)) // 错: 想打栈只剩收尾帧 stack := debug.Stack() // 对: 先拍照再处理 log.Error(toErr(r), "stack", stack) }
func h() (err error) { data := decode(raw) // 错: 这行 panic 时 defer 未注册 defer func() { if r := recover(); r != nil { err = toErr(r) } }() use(data) // 对: recover defer 永远写函数第一行 }
recover() 返回 nil, if r := recover(); r != nil 判不出发生过 panic. 正解: 升 1.21+ (自动转 *runtime.PanicNilError) 且永远只 panic 非 nil 值。panic(nil) // 错: 1.21 前 recover() → nil, r != nil 判不出 // 对: 只 panic 非 nil 值; 1.21+ 转 *runtime.PanicNilError
// 错: net/http 自带 recover 只记日志 + 断连接 // 客户端收到 connection reset, 不是 500 defer func() { // 对: 自己的中间件写回 500 if r := recover(); r != nil { w.WriteHeader(http.StatusInternalServerError) } }()
i := i 或参数传入定格。for _, v := range []int{1, 2, 3} { v := v // 对: 老版本每轮定格副本 defer func() { use(v) }() // 错: 1.22 前闭包全拿最终值 }
return err, 收口函数统一 defer flush 后再 os.Exit。func main() { defer log.Sync() // 错: log.Fatal 后不执行 if err := run(); err != nil { log.Fatal(err) // 直接退出, buffer 丢 } } // 对: main 只 return err, 收口 defer flush 后再 os.Exit
t.Error + return; panic 场景让外层 recover 断言。go func() { t.Fatal("bad") // 错: 子 G 里触发 Goexit, 挂死 if err != nil { t.Errorf("bad: %v", err) // 对: 只记错后 return } }()
debug.Stack() 再干活。defer func() { m["k"] = 1 // 错: m 为 nil, 新 panic 覆盖现场 }() defer func() { defer func() { _ = recover() }() // 对: defer 里也防御 cleanup() }()
if retriable { err = nil }, 正常错误被抹掉, 监控全绿但数据没写进去. 正解: defer 只做补充包装/记录, 不做消除错误的判断。defer func() { if retriable(err) { err = nil // 错: 数据没写进去, 监控全绿 } err = wrapCtx(err) // 对: 只做包装/记录 }()
panic("recovered"), 上层只看到新值, 原始错误链断了. 正解: panic(r) 原值再抛, 需要带上下文时用 fmt.Errorf("%w", ...) 包装后 panic。if r := recover(); r != nil { panic("recovered") // 错: 原始错误链断了 } // 对: panic(r) 原值再抛; 带上下文: // panic(fmt.Errorf("ctx: %w", r.(error)))