recover 必须在 defer 中调用才有效,且需在 panic 发生前注册;它仅在 panic 传播中、goroutine 未退出时生效,否则返回 nil;配合 debug.Stack() 可获取完整堆栈,但需在 recover 后立即调用。

recover 必须在 defer 中调用才有效
Go 的 recover 不是“捕获异常”的通用机制,它只在 panic 正在传播、且当前 goroutine 还没退出时起作用。如果不在 defer 函数里调用,recover 永远返回 nil —— 这是最常见的误用点。
常见错误现象:recover() 返回 nil,但程序仍 panic 退出;或在普通函数里调用,完全无效果。
-
recover()必须紧贴defer,且该defer必须在 panic 发生前已注册(即 panic 前已执行到对应 defer 语句) - 不能在嵌套的非 defer 函数中调用
recover,哪怕它被 defer 函数调用也不行 - 每个 goroutine 独立处理自己的 panic,主 goroutine 的 defer 不会捕获子 goroutine 的 panic
打印完整堆栈要用 runtime/debug.Stack()
recover() 只返回 panic 的值(比如 errors.New("xxx")),不带位置信息。要看到哪一行 panic 的,必须配合 runtime/debug.Stack() —— 它返回当前 goroutine 的完整调用栈字节切片。
注意:这不是标准库的 fmt.PrintStack()(它直接输出到 os.Stderr,不可控),而是可捕获、可格式化的原始栈数据。
立即学习“go语言免费学习笔记(深入)”;
- 调用
runtime/debug.Stack()最好放在recover()后立即执行,否则栈可能因后续 defer 执行而变化 - 返回值是
[]byte,需用string()转换才能打印或记录 - 在生产环境建议限制栈长度(如截取前 4KB),避免日志爆炸;开发期可全量保留
defer func() {
if r := recover(); r != nil {
log.Printf("panic: %v\n%s", r, string(debug.Stack()))
}
}()
recover 后继续 panic 会丢失原始栈
有些场景需要“拦截 panic → 记录 → 再抛出”,比如中间件统一日志后交由上层处理。但直接 panic(r) 会导致原始栈被重置为当前行,debug.Stack() 也变成新 panic 的栈。
正确做法是用 runtime.Goexit() 配合手动 panic,或者更稳妥地——不重新 panic,而是返回错误并让调用方决定是否继续失败。
- 若必须重抛,可用
panic(r),但得接受栈信息丢失;debug.Stack()应在 panic 前获取 - 不要在 recover 后再调用其他可能 panic 的函数(如 map 写 nil、索引越界),否则新 panic 会覆盖原信息
- goroutine 泄漏风险:recover 后若未显式 return 或 exit,后续代码仍会执行,可能引发意外状态
HTTP handler 中 recover 的典型安全封装
Web 服务最常需要全局 panic 捕获,但直接在每个 handler 里写 defer 太重复。推荐封装成中间件,但要注意:必须在 handler 函数体内 defer,不能只在中间件函数里 defer —— 因为 handler 执行时 panic 才发生。
错误写法:func middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer recover() {...}; next.ServeHTTP(...) }) } —— 这里的 defer 在中间件函数作用域,不是 handler 执行时的栈帧。
- 正确封装:中间件返回的 handler 函数内部必须有
defer,且该defer在next.ServeHTTP前注册 - panic 后应设 HTTP 状态码(如 500),并关闭响应流,防止后续 write 导致 “http: response.WriteHeader called multiple times”
- 敏感信息过滤:
debug.Stack()可能含路径、变量名,上线前建议正则清洗或仅记录摘要



















