Go HTTP服务中panic直接返回500是因为http.ServeHTTP不捕获handler内panic,仅打印简单日志;必须在handler最外层用defer+recover显式恢复,记录堆栈并写入500响应。

Go HTTP 服务中 panic 为什么会直接 500?
Go 的 http.ServeHTTP 不捕获 handler 内部 panic,一旦发生,连接会直接关闭,日志里只留一句 http: panic serving ...,没有堆栈、无法定位。这不是 bug,是 Go 明确的设计选择:不隐式恢复,避免掩盖逻辑错误。
所以“全局错误恢复”不是加个中间件就能一劳永逸,而是要在 http.Handler 链路最外层做显式 recover,并控制响应行为和日志粒度。
用 defer + recover 包裹 handler 是最简方案
核心思路:把原始 http.Handler 封装进一个新 handler,在其 ServeHTTP 方法开头 defer 一个 recover 调用。注意必须在函数作用域内 defer,不能在 goroutine 里做。
-
recover()只对当前 goroutine 有效,而 HTTP handler 每次请求都在独立 goroutine 中运行,所以每个 handler 实例都要有自己的 defer - 不要在
init()或全局变量初始化时 recover —— 那里 panic 是启动失败,该挂就挂 - recover 后建议记录完整堆栈:
debug.PrintStack()或runtime/debug.Stack(),否则只拿到 panic 值,很难调试 - 恢复后必须写响应,否则连接卡住;推荐统一返回
http.StatusInternalServerError,不要试图“继续执行”
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC in %s %s: %+v", r.Method, r.URL.Path, err)
debug.PrintStack()
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
为什么 gin.ErroHandler 或 chi.Middleware 不够用?
第三方框架的 recovery 中间件(如 gin.Recovery()、chi.Middlewares.Recoverer)本质也是 defer+recover,但它们默认行为有隐患:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- gin 默认 recovery 会吞掉所有 panic 日志,只打一行
recovered from panic,没堆栈也没请求上下文 - chi 的
Recoverer在 panic 后调用http.Error,但不保证响应头未写入 —— 如果 panic 发生在 writeHeader 之后,可能触发http: multiple response.WriteHeader calls - 它们通常不区分开发/生产环境:开发时需要堆栈,生产时应避免泄露敏感信息,需手动覆盖
Println行为
所以更稳妥的做法是自己写 recovery handler,或至少重写框架 recovery 的日志输出逻辑,比如给 gin.RecoveryWithWriter 传自定义 io.Writer。
recover 无法捕获的 panic 场景要单独处理
有些 panic 根本不在 HTTP handler goroutine 里,recover 完全无效:
- goroutine 泄漏导致的
runtime: goroutine stack exceeds 1000000000-byte limit—— 这类 OOM 级 panic 无法 recover,只能靠 pprof 和限流预防 - 在
init()、包加载、CGO 调用中发生的 panic —— 这些发生在 main goroutine 或 runtime 初始化阶段,不属于 handler 生命周期 - 使用
os.Exit()或直接调用C.exit()—— 这不是 panic,recover 完全不生效
真正需要 recovery 的,只是业务 handler 函数体内部的非预期 panic。其他情况靠监控、测试、资源限制更有效。
实际部署时,recover 只是最后一道兜底,重点还是写好 context 超时、避免空指针解引用、不用全局可变状态 —— 那些才是 panic 的主要来源。

















