Go HTTP中间件应限制recover范围仅包裹next.ServeHTTP(),只捕获预期内业务panic并转为错误响应;通过上下文错误指针或第三方库定制化处理error返回值,避免吞严重错误或破坏分层。
直接用 recover() 捕获 panic 是最常见也最容易出错的做法。中间件里不加判断地 defer-recover,会吞掉本该让服务崩溃的严重错误(比如空指针解引用、内存溢出),反而掩盖问题。
正确做法是只 recover 预期内的业务 panic(比如手动 panic(errors.New("validation failed"))),并限制 recover 范围——仅包裹 next.ServeHTTP() 调用,而非整个中间件函数体。
err := recover(),再判断 err 类型:若为 error 或实现了 Error() 方法的自定义类型,才转为 HTTP 响应;其他值(如 string、int)建议 log 后重新 panicnext.ServeHTTP() —— 请求已中断,重复执行会导致 header 已写入等错误StatusCode() int 方法,或用错误包装(如 errors.Join(httpErr, validationErr))配合解析逻辑Go 的 error 是返回值,不是异常,中间件本身拿不到 handler 函数内部的 return err。想统一处理,必须改变 handler 签名或注入上下文。
推荐用「错误收集上下文」模式:在请求上下文中存一个 *error 指针,handler 内部遇到错误时写入它,中间件在 next.ServeHTTP() 后检查该指针是否非 nil。
ctx = context.WithValue(r.Context(), errorKey{}, &err),其中 errorKey 是私有类型,避免 key 冲突return err,而是 *ctx.Value(errorKey{}).(*error) = err
if e := *ctx.Value(errorKey{}).(*error); e != nil { http.Error(w, e.Error(), statusCode(e)) }
chi、gin、echo 等框架的错误中间件默认只打印日志或返回简单文本,无法满足 API 错误码、i18n、traceID 注入等需求。
以 chi 为例,其 middleware.Recoverer 默认用 http.Error(w, ...),但你可以替换它的 RecoverFunc 字段,完全控制错误序列化逻辑。
mux.Use(middleware.RecovererWithWriter(&customWriter{})),其中 customWriter 实现 WriteError(w http.ResponseWriter, err error)
WriteError 中可获取当前请求:r := http.RequestFromContext(ctx),从而提取 X-Request-ID、Accept 头决定返回 JSON 还是 plain textlog.Printf:改用结构化 
zerolog.Ctx(r.Context()))自动带上 traceID 和路径信息Recoverer 不处理 handler 返回的 error,它只管 panic;返回值错误仍需额外中间件拦截中间件通常位于请求生命周期靠前的位置,而业务错误往往在深层 handler 或 service 层才生成。过早用 errors.Is(err, mypkg.ErrNotFound) 会强制中间件依赖业务包,破坏分层,也导致错误分类逻辑分散。
更合理的方式是让错误携带语义标签(如 HTTP 状态码、错误类别),而不是具体类型。
type StatusCoder interface { StatusCode() int },业务 error 实现它,中间件只调用 sc.StatusCode()
errors.Join(err, httpErr(404)),中间件遍历 errors.Unwrap 找第一个 httpErr 值os.IsTimeout、net.ErrClosed)——这些应由 infra 层提前转换,而非暴露给 web 中间件错误中间件最难的不是捕获,而是决定什么该被拦截、什么该冒泡、什么该记录后忽略。多数线上问题都出在 recover 范围过大,或把临时网络错误当成业务失败返回给前端。