recover必须在defer中调用,因为仅当goroutine处于panic状态且defer函数正在执行时,recover才有效;普通函数中调用恒返回nil。

为什么 recover 必须在 defer 中调用
Go 的 recover 只有在 defer 函数中执行才有效,直接在普通函数里调用会返回 nil。中间件本质是闭包嵌套的 handler,panic 发生时 goroutine 正在执行某个 handler,必须在该 handler 所在栈帧里提前埋好 defer,才能捕获到 panic。
常见错误是把 recover 放在中间件外层函数体里,或者放在 handler 返回后才执行的逻辑中——这时栈已展开,recover 失效。
-
recover()必须紧贴在defer后面,且不能被条件语句包裹(比如if err != nil { defer recover() }是错的) - 中间件要 wrap 每个 handler,不是只 wrap 一次顶层路由
- 如果用了第三方路由库(如
gin、echo),它们内部已封装recover,自行再加一层可能重复或冲突
标准 HTTP 中间件中正确使用 defer + recover
最简可靠写法是在中间件闭包内对每个请求启动独立的 defer 链:
func RecoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// 记录 panic 堆栈
log.Printf("Panic recovered: %+v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
注意点:
- 必须在
next.ServeHTTP调用前注册defer,否则 panic 时 handler 已退出,无法捕获 -
recover()返回的是interface{},不是error,不能直接用errors.Is判断;需类型断言或用fmt.Sprintf("%+v", err)输出完整信息 - 不要在 defer 里做耗时操作(如写磁盘日志),可能拖慢错误响应;建议先记录到内存 buffer 或异步 channel
如何获取 panic 的完整堆栈(不只是错误值)
recover() 只返回 panic 的参数,不带堆栈。要拿到 panic 发生位置的完整 trace,得靠 debug.PrintStack() 或 runtime/debug.Stack():
defer func() {
if r := recover(); r != nil {
buf := make([]byte, 4096)
n := runtime.Stack(buf, false)
log.Printf("Panic: %+v\nStack:\n%s", r, buf[:n])
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
关键区别:
-
runtime.Stack(buf, false)只打当前 goroutine 的栈,开销小,适合线上 -
runtime.Stack(buf, true)打所有 goroutine 栈,调试用,别在线上开 - 不要依赖
log.Panic或log.Fatal替代recover—— 它们会终止整个进程
goroutine 泄漏风险:panic 后未关闭的 responseWriter 和 context
panic 可能发生在 handler 写响应中途,此时 http.ResponseWriter 可能已写部分 header/body,但连接未关闭。更危险的是,如果 handler 启动了子 goroutine 并传入了 r.Context(),panic 后父 context 被 cancel,子 goroutine 却可能还在运行(尤其用了 go func() {...}() 且没做 done channel 控制)。
应对方式:
- 在
recover分支里检查w.Header().Get("Content-Type")是否已设置,避免重复写 header 导致http: multiple response.WriteHeader calls - 子 goroutine 必须监听
r.Context().Done(),并在select中处理 cancel 或 timeout - 不要在中间件里用
context.WithCancel(r.Context())后忘记 defer cancel —— panic 会让 defer 不执行,导致 context 泄漏
真正难的不是捕获 panic,而是确保 panic 后状态可预测:连接是否关闭、资源是否释放、日志是否落盘、监控是否上报。这些细节不处理,中间件就只是个“假装兜底”的装饰器。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











