必须在中间件最外层用defer+recover捕获panic,记录堆栈并返回500错误;error应通过context传递由统一错误处理器响应,避免中间件直接写响应;禁用log.Fatal/os.Exit以防进程退出。

中间件里 panic 了怎么办
Go 的 HTTP 中间件本身不捕获 panic,一旦 panic 发生,整个请求协程会终止,连接可能被意外关闭,日志也不一定留下痕迹。这不是“错误处理”,是服务不稳定源。
必须在中间件最外层加 recover(),且只应在 HTTP 请求生命周期内做——不能在 goroutine 或定时任务里盲目 recover。
- recover 必须紧跟在
defer后,且 defer 必须在 handler 执行前注册 - recover 只对当前 goroutine 有效,不要试图跨 goroutine 捕获
- recover 后应返回标准 HTTP 错误(如 500),并记录 panic 堆栈(用
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)
})
}
如何把 error 转成 HTTP 响应
中间件不该直接调用 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) - 最外层中间件检查 ctx 中是否有
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})
}
})
}
中间件链中 error 传播的常见陷阱
很多开发者在中间件里调用 next.ServeHTTP() 后继续执行后续代码,却忽略了:如果下游 handler 已经写了响应头和 body,再写就会 panic(http: multiple response.WriteHeader calls)。
10分钟内自己学会PHP其中,第1篇为入门篇,主要包括了解PHP、PHP开发环境搭建、PHP开发基础、PHP流程控制语句、函数、字符串操作、正则表达式、PHP数组、PHP与Web页面交互、日期和时间等内容;第2篇为提高篇,主要包括MySQL数据库设计、PHP操作MySQL数据库、Cookie和Session、图形图像处理技术、文件和目录处理技术、面向对象、PDO数据库抽象层、程序调试与错误处理、A
更隐蔽的问题是:你认为“出错就 return”,但没考虑中间件本身可能被嵌套多层,return 只退出当前函数,不会中断整个链。
- 不要在
next.ServeHTTP()后写响应逻辑,除非你明确知道下游没写过 - 避免用 “if err != nil { return }” 风格跳过后续逻辑;改用 “if err != nil { …; return }” 并确保所有分支都终止
- 若需提前终止链(如鉴权失败),应直接写响应并 return,不要依赖下游“不执行”
为什么不要在中间件里用 log.Fatal 或 os.Exit
log.Fatal 和 os.Exit 会终止整个进程,不是单个请求。哪怕只在一个请求里触发,也会干掉所有正在处理的连接、未 flush 的日志、后台 goroutine。
真实场景中,这类调用往往藏在第三方库的“兜底错误处理”里,比如某 SDK 遇到配置缺失就 log.Fatal("missing key") —— 这类代码必须包装或替换。
- 所有中间件内出现的错误,都应降级为 HTTP 响应 + 日志记录
- 启动期配置错误可以
log.Fatal,但运行时请求期绝对不行 - 用
go vet或staticcheck扫描项目,查log.Fatal/os.Exit是否出现在 handler 或中间件函数内









