recover只对当前goroutine有效且必须在defer中调用,写在handler开头或中间件外层无效;根本原因是它仅在panic传播中、同一goroutine的defer栈帧内才生效。

recover 在 Go 的 HTTP 控制器里不能“兜底全局”,它只对当前 goroutine 有效,且必须在 defer 中调用——写在 handler 函数体开头、或放在中间件外层的裸 recover() 都不会生效。
为什么控制器里的 recover 总是返回 nil
常见现象:在 http.HandlerFunc 里直接写 recover(),或者在中间件里没用 defer 包一层,结果永远拿到 nil。根本原因是:recover() 只有在 panic 正在传播、且当前 goroutine 尚未退出、且调用点位于 panic 发生的同一栈帧(或其 defer)时才可能成功。
- HTTP server 启动后,每个请求由独立 goroutine 处理,
panic发生在该 goroutine 内,但若没在该 goroutine 的 handler 函数内设defer,就错失捕获时机 - 中间件函数本身不是 handler 的 defer 上下文,必须显式用
defer func() { ... }()包裹recover - 如果 panic 发生在另一个 goroutine(比如
go doAsync()里),当前 handler 的recover完全无感
标准 controller recover 模式(带命名返回值)
最稳妥的做法是在每个 handler 函数内部设 defer,并利用命名返回值把错误带出来。这样既能捕获 panic,又不破坏 HTTP 状态码和响应体控制权。
func userHandler(w http.ResponseWriter, r *http.Request) {
// 命名返回 err,便于 defer 中赋值
var err error
defer func() {
if r := recover(); r != nil {
// 记录 panic(建议用结构化日志)
log.Printf("panic in userHandler: %v", r)
// 统一转成 500 错误响应
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
err = fmt.Errorf("handler panic: %v", r)
}
}()
// 实际业务逻辑,可能 panic
data := riskyParse(r.Body)
json.NewEncoder(w).Encode(data)
}
- 必须用命名返回值(如
var err error),否则defer中修改的变量对外不可见 - 不要在
recover后继续执行后续 handler 逻辑——panic已让控制流中断,后续语句不会运行 - 避免在
recover块里再调用可能 panic 的函数(比如又写个json.Marshal),否则二次 panic 无法被捕获
中间件方式统一 recover(需注意 goroutine 边界)
想复用 recover 逻辑?可以封装成中间件,但必须确保 defer 注册在 handler 所在 goroutine 内——也就是在调用 next.ServeHTTP 前注册。
func RecoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic recovered: %v", r)
http.Error(w, "Service Unavailable", http.StatusServiceUnavailable)
}
}()
next.ServeHTTP(w, r) // panic 必须发生在这里或其调用链中
})
}
- 这个中间件只对
next.ServeHTTP调用链中的 panic 有效;如果下游 handler 启了新 goroutine 并在其中 panic,这里捕获不到 - 不要试图在中间件里恢复 panic 后“重试” handler——Go 不支持执行流回滚,
next.ServeHTTP已退出,重调会重复处理请求 - 若 handler 中用了
http.TimeoutHandler或类似包装,要确认 panic 是否仍发生在同一 goroutine(通常仍是)
recover 捕获后无法做的事
很多人误以为 recover 是 Go 的 try-catch,其实它只是“止血”,不是“续命”。以下操作全部无效:
-
recover后继续执行panic行之后的代码(比如fmt.Println("after panic"))——这行永远不会运行 - 尝试回滚已发生的副作用:文件已写、DB 已提交、channel 已发送、map 已修改,这些都无法撤销
- 在
recover块里调用http.Redirect或设置 header——此时 response writer 可能已被部分写入,再操作会 panic - 把
recover当常规错误处理用:比如对json.Unmarshal失败也 panic,这违背 Go 错误处理哲学,应优先返回error
真正关键的一点是:recover 不是容错机制,而是最后防线。它救不了设计缺陷,也掩盖不了本该用 if err != nil 显式检查的错误。线上服务里,recover 日志出现频率高,往往说明底层逻辑没做好防御,而不是 recover 写得不够多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











