必须在中间件最外层用 defer + recover(),因为 recover 只对当前 goroutine 生效且仅能在 defer 中调用;若 panic 发生在 c.Next() 后、新 goroutine 中或 handler 已返回,则无法捕获。

必须在中间件最外层用 defer + recover(),且确保它包裹了真正的 handler 执行;否则 panic 会直接冒泡到 net/http 默认逻辑,返回空响应或泄露堆栈。
为什么 defer + recover() 必须写在中间件最外层
recover 只对当前 goroutine 生效,且只能在 defer 函数中调用。如果 panic 发生在中间件内部(比如日志写入失败)、或发生在 c.Next() 之后、或 handler 已经 return 之后,recover() 就捕不到。
常见错误写法:
- 把
defer func() { recover() }()放在c.Next()后面 —— 此时 panic 已经发生并退出函数,defer 不会执行 - 中间件没真正 wrap 到最终 handler,比如用
http.HandleFunc直接注册,绕过了你的中间件链 - 用了 Gin/Echo 等框架,但没确认它们的 middleware 执行顺序,导致你的 recovery 中间件被跳过
标准中间件写法(net/http 原生)
核心是:注册一个 wrapper,里面 defer 在 next.ServeHTTP 前声明,且显式写响应。
立即学习“go语言免费学习笔记(深入)”;
示例:
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
w.WriteHeader(500)
json.NewEncoder(w).Encode(map[string]string{
"error": "internal server error",
})
// 注意:这里必须 log,但别把 err 直接暴露给前端
log.Printf("PANIC in %s %s: %+v", r.Method, r.URL.Path, err)
}
}()
next.ServeHTTP(w, r)
})
}
关键点:
-
defer必须在next.ServeHTTP之前定义 - 捕获后必须调用
w.WriteHeader(500)和w.Write(或json.NewEncoder(w).Encode),否则客户端会卡住等超时 - 不要只
log.Print就完事 —— HTTP 连接不会自动关闭,响应体为空会导致前端 hung
Gin 框架下替换默认 gin.Recovery()
Gin 自带的 gin.Recovery() 只打日志,不写响应体。要统一 JSON 错误,得重写。
示例:
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
c.JSON(500, gin.H{
"code": 500,
"message": "internal server error",
})
c.Abort() // 阻止后续中间件和 handler 执行
log.Printf("PANIC: %+v\n%s", err, debug.Stack())
}
}()
c.Next()
}
}
注意:
- 必须 import
"runtime/debug"才能用debug.Stack() -
c.Abort()很关键,否则 panic 后仍可能执行下游逻辑 - 开发环境可加
"debug": string(debug.Stack())字段,生产环境务必删掉
最容易漏掉的三个点
实际部署中最常出问题的不是“怎么写”,而是“写完忘了什么”:
-
recover()后没调w.WriteHeader()和w.Write()—— 客户端收不到任何响应,连接挂起 - 把业务错误(如
errors.New("invalid id"))和 panic 混在一起处理,结果所有错误都报 500,监控告警失真 - panic 发生在新 goroutine 里(比如
go func() { ... panic() }()),这个中间件完全捕不到 —— recover 只作用于当前 goroutine
事情说清了就结束。真正难的不是加一行 defer recover(),而是确保它在正确的位置、正确的时机、做正确的事 —— 尤其是响应写出和错误分类这两步,一漏就进线上排查黑洞。


















