Go如何在中间件中处理错误_Go Web错误处理流程说明

必须在中间件最外层用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.Err

or() 或写响应体,而应把错误“传递下去”,由统一的错误处理器收口。否则各中间件各自写状态码、写 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)。

更隐蔽的问题是:你认为“出错就 return”,但没考虑中间件本身可能被嵌套多层,return 只退出当前函数,不会中断整个链。

  • 不要在 next.ServeHTTP() 后写响应逻辑,除非你明确知道下游没写过
  • 避免用 “if err != nil { return }” 风格跳过后续逻辑;改用 “if err != nil { …; return }” 并确保所有分支都终止
  • 若需提前终止链(如鉴权失败),应直接写响应并 return,不要依赖下游“不执行”

为什么不要在中间件里用 log.Fatal 或 os.Exit

log.Fatalos.Exit 会终止整个进程,不是单个请求。哪怕只在一个请求里触发,也会干掉所有正在处理的连接、未 flush 的日志、后台 goroutine。

真实场景中,这类调用往往藏在第三方库的“兜底错误处理”里,比如某 SDK 遇到配置缺失就 log.Fatal("missing key") —— 这类代码必须包装或替换。

  • 所有中间件内出现的错误,都应降级为 HTTP 响应 + 日志记录
  • 启动期配置错误可以 log.Fatal,但运行时请求期绝对不行
  • go vetstaticcheck 扫描项目,查 log.Fatal / os.Exit 是否出现在 handler 或中间件函数内
实际最难的不是写 recover,而是判断一个 error 到底该透传、该转成 400、还是该记日志后忽略。这取决于它是否影响语义、是否可重试、是否暴露敏感信息——这些没法靠中间件自动决定,得靠每个 handler 自己标注清楚。