必须在中间件最外层用defer+recover捕获panic,记录堆栈并返回500错误;error应通过context传递由统一错误处理器响应,避免中间件直接写响应;禁用log.Fatal/os.Exit以防进程退出。
Go 的 HTTP 中间件本身不捕获 panic,一旦 panic 发生,整个请求协程会终止,连接可能被意外关闭,日志也不一定留下痕迹。这不是“错误处理”,是服务不稳定源。
必须在中间件最外层加 recover(),且只应在 HTTP 请求生命周期内做——不能在 goroutine 或定时任务里盲目 recover。
defer 后,且 defer 必须在 handler 执行前注册debug.PrintStack() 或 log.Printf("%+v", err))func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
log.Printf("PANIC in %s %s: %+v", r.Method, r.URL.Path, err)
debug.PrintStack()
}
}()
next.ServeHTTP(w, r)
})
}
中间件不该直接调用 http.Error() 或写响应体,而应把错误“传递下去”,由统一的错误处理器收口。否则各中间件各自写状态码、写 body,容易冲突或遗漏 Content-Type。
推荐用自定义 error 类型 + context 传递,例如:
type AppError struct { Code int; Message string; Err error }
error,如果是 *AppError,就设置 ctx = context.WithValue(r.Context(), appErrorKey, err)
AppError,有则统一写响应func ErrorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
if err, ok := r.Context().Value(appErrorKey).(*AppError); ok {
w.Header().Set("Content-Type", "application/json"
)
w.WriteHeader(err.Code)
json.NewEncoder(w).Encode(map[string]string{"error": err.Message})
}
})
}
很多开发者在中间件里调用 next.ServeHTTP() 后继续执行后续代码,却忽略了:如果下游 handler 已经写了响应头和 body,再写就会 panic(http: multiple response.WriteHeader calls)。
更隐蔽的问题是:你认为“出错就 return”,但没考虑中间件本身可能被嵌套多层,return 只退出当前函数,不会中断整个链。
next.ServeHTTP() 后写响应逻辑,除非你明确知道下游没写过log.Fatal 和 os.Exit 会终止整个进程,不是单个请求。哪怕只在一个请求里触发,也会干掉所有正在处理的连接、未 flush 的日志、后台 goroutine。
真实场景中,这类调用往往藏在第三方库的“兜底错误处理”里,比如某 SDK 遇到配置缺失就 log.Fatal("missing key") —— 这类代码必须包装或替换。
log.Fatal,但运行时请求期绝对不行go vet 或 staticcheck 扫描项目,查 log.Fatal / os.Exit 是否出现在 handler 或中间件函数内