recover 只在当前 goroutine 的 panic 栈回溯瞬间有效,且必须置于 defer 匿名函数体内;提前赋值、子 goroutine 跨越调用或 defer 位置错误均导致捕获失效。

recover 只在当前 goroutine 有效,且必须出现在 defer 函数体内——不满足这两点,panic 永远不会被捕获。线上服务崩溃后查日志只看到 http: panic serving 而无上下文,基本就是 recovery 位置或作用域错了。
为什么 recover 必须写在 defer 函数里
recover 的设计约束很硬:它只在 panic 正在发生、栈正在回溯、且当前 goroutine 尚未退出的那一个瞬间有效。一旦函数返回,这个窗口就关闭了。
常见错误包括:
- 把
recover()提前赋值给变量,比如r := recover()放在 defer 外面 → 永远是nil - 用命名返回值 + defer 修改,但 defer 里没调
recover()→ 白忙活 - 在子 goroutine 里 panic,却指望外层 handler 的 defer 捕获 → 不可能,goroutine 隔离
正确姿势只有一种:defer func() { if r := recover(); r != nil { /* 处理 */ } }()。注意括号:这是立即执行的匿名函数,不是普通函数声明。
HTTP handler 中 recovery 中间件的关键细节
Go 的 http.Server 默认不兜底,panic 只会静默关闭连接,前端收不到 500,日志也缺 traceID。必须显式加中间件,但有三个硬坑:
-
defer必须放在next.ServeHTTP()之前,否则注册无效 - 如果中间件已写入响应头(
w.Header().WasWritten()返回 true),再调http.Error()会二次 panic - 用
debug.Stack()拿原始堆栈,别用debug.PrintStack()(直打 stderr,无法接入结构化日志)
示例中关键判断是:if !w.Header().WasWritten() { http.Error(w, "Internal Server Error", http.StatusInternalServerError) }。漏掉这句,高并发下容易雪崩。
gRPC 拦截器链里 recovery 的位置不能错
在 gRPC UnaryServerInterceptor 链中,recover 必须是**最外层拦截器**。原因很实际:
- 如果
Tracing或Auth拦截器先执行,它们内部 panic(比如空指针解引用),而 recovery 在内层,进程直接 crash -
Tracing必须早于Logging和Auth:只有先解析出trace_id,后续日志和鉴权失败才能带上上下文
所以标准顺序是:Recovery → Tracing → Logging/Auth → Handler。任何调换都可能导致可观测性断裂或服务不可用。
goroutine 内 panic 的恢复必须隔离生命周期
想让一组长期运行的 worker(比如从 channel 消费任务)自动重启,不能在循环里简单 defer recover() 然后 go f() —— 这会导致 goroutine 泄漏和计数器竞争。
正确做法是封装带状态的恢复器:
func recoverer(maxPanics, id int, f func()) {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Worker %d panicked: %v\n", id, r)
if maxPanics <p>每个 worker 拥有独立的 <code>maxPanics</code> 计数和 <code>id</code>,重启逻辑不污染全局状态。这也是为什么你不该在 <code>printer</code> 函数内部自己做 recover —— 它应该只管业务,恢复交给上层封装。</p><p>真正难的不是写对一行 <code>recover()</code>,而是想清楚 panic 发生在哪一层、由谁负责清理、恢复后是否还能继续提供服务。线上环境里,一次没 catch 住的 panic,代价远不止一个 goroutine 崩溃。</p>golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











