Go HTTP中间件应限制recover范围仅包裹next.ServeHTTP(),只捕获预期内业务panic并转为错误响应;通过上下文错误指针或第三方库定制化处理error返回值,避免吞严重错误或破坏分层。

Go HTTP 中间件如何统一捕获 panic 并转为错误响应
直接用 recover() 捕获 panic 是最常见也最容易出错的做法。中间件里不加判断地 defer-recover,会吞掉本该让服务崩溃的严重错误(比如空指针解引用、内存溢出),反而掩盖问题。
正确做法是只 recover 预期内的业务 panic(比如手动 panic(errors.New("validation failed"))),并限制 recover 范围——仅包裹 next.ServeHTTP() 调用,而非整个中间件函数体。
- 在 defer 函数中先
err := recover(),再判断err类型:若为error或实现了Error()方法的自定义类型,才转为 HTTP 响应;其他值(如string、int)建议 log 后重新 panic - 不要在 recover 后继续调用
next.ServeHTTP()—— 请求已中断,重复执行会导致 header 已写入等错误 - 响应状态码别硬编码 500:可约定 panic 错误实现
StatusCode() int方法,或用错误包装(如errors.Join(httpErr, validationErr))配合解析逻辑
如何把 error 返回值透传到中间件做统一处理
Go 的 error 是返回值,不是异常,中间件本身拿不到 handler 函数内部的 return err。想统一处理,必须改变 handler 签名或注入上下文。
推荐用「错误收集上下文」模式:在请求上下文中存一个 *error 指针,handler 内部遇到错误时写入它,中间件在 next.ServeHTTP() 后检查该指针是否非 nil。
立即学习“go语言免费学习笔记(深入)”;
- 初始化上下文:
ctx = context.WithValue(r.Context(), errorKey{}, &err),其中errorKey是私有类型,避免 key 冲突 - handler 中不再
return err,而是*ctx.Value(errorKey{}).(*error) = err - 中间件末尾读取:
if e := *ctx.Value(errorKey{}).(*error); e != nil { http.Error(w, e.Error(), statusCode(e)) } - 注意:此方式要求所有 handler 都遵守约定,适合团队规范强的项目;否则不如显式返回 error 并用包装器函数统一处理
使用第三方库(如 chi/middleware)时如何定制错误响应格式
chi、gin、echo 等框架的错误中间件默认只打印日志或返回简单文本,无法满足 API 错误码、i18n、traceID 注入等需求。
动态WEB网站中的PHP和MySQL详细反映实际程序的需求,仔细地探讨外部数据的验证(例如信用卡卡号的格式)、用户登录以及如何使用模板建立网页的标准外观。动态WEB网站中的PHP和MySQL的内容不仅仅是这些。书中还提到如何串联JavaScript与PHP让用户操作时更快、更方便。还有正确处理用户输入错误的方法,让网站看起来更专业。另外还引入大量来自PEAR外挂函数库的强大功能,对常用的、强大的包
以 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 text - 避免直接调用
log.Printf:改用结构化 logger(如zerolog.Ctx(r.Context()))自动带上 traceID 和路径信息 - 注意:chi 的
Recoverer不处理 handler 返回的 error,它只管 panic;返回值错误仍需额外中间件拦截
为什么不要在中间件里用 errors.Is 判断具体错误类型
中间件通常位于请求生命周期靠前的位置,而业务错误往往在深层 handler 或 service 层才生成。过早用 errors.Is(err, mypkg.ErrNotFound) 会强制中间件依赖业务包,破坏分层,也导致错误分类逻辑分散。
更合理的方式是让错误携带语义标签(如 HTTP 状态码、错误类别),而不是具体类型。
- 定义错误接口:
type StatusCoder interface { StatusCode() int },业务 error 实现它,中间件只调用sc.StatusCode() - 或用错误包装:
errors.Join(err, httpErr(404)),中间件遍历errors.Unwrap找第一个httpErr值 - 避免跨层 import:中间件不应 import model 或 service 包,否则一次业务错误变更就要改中间件
- 真正难处理的是底层 I/O 错误(如
os.IsTimeout、net.ErrClosed)——这些应由 infra 层提前转换,而非暴露给 web 中间件
错误中间件最难的不是捕获,而是决定什么该被拦截、什么该冒泡、什么该记录后忽略。多数线上问题都出在 recover 范围过大,或把临时网络错误当成业务失败返回给前端。









