中间件错误传播本质是人为设计,需显式返回或拦截error:应统一用*apperror包装、通过context透传traceid、最外层recover捕获panic并手动写响应,避免全局变量和裸error通信。

中间件错误传播本质是 error 值未被显式返回或拦截
原生 net/http 中间件链不自动传递 error,每个中间件只负责调用 next.ServeHTTP(),而 handler 本身不返回 error。所以“错误传播”其实是人为设计的——要么靠 panic + recover 捕获,要么让 handler 改为返回 error,再由外层中间件读取。Gin/Echo 等框架虽提供 c.Error() 或 c.AbortWithError(),但它们只是把 error 存进 context,仍需你主动在最后统一响应。
- 不要依赖框架“自动转发 error”:Gin 的
c.Error()只是缓存,不终止流程;必须配c.Abort()或后续中间件检查c.Errors.Len() > 0 - 自定义中间件若调用了第三方库(如 JWT 验证),它内部 panic 或返回 error,你得在自己的中间件里加
defer recover或显式判断其返回值 - 多个中间件顺序很重要:鉴权中间件应在日志中间件之后、业务 handler 之前;否则日志可能记不到失败原因
用 AppError 统一包装所有中间件输出的错误
中间件之间不该用字符串或裸 error 通信,而应约定返回 *AppError 类型。比如鉴权中间件发现 token 过期,不直接 http.Error(w, ...),而是返回 NewAppError(401, "token expired", "auth.verify"),由最外层中间件统一处理。
- 所有中间件函数签名建议改为
func(http.ResponseWriter, *http.Request) error,而非 void - 中间件内调用下游时,要检查返回的
err,并决定是否继续执行(例如限流中间件返回ErrRateLimited就该立即返回,不调用 next) - 避免在中间件里对
errors.Is(err, xxx)做业务分支——那是 handler 层的事;中间件只做“拦截→包装→透传”
recover 必须在最外层中间件,且不能漏写 w.WriteHeader()
中间件嵌套时,recover 只捕获当前 goroutine 内、同一函数中发生的 panic。如果某个中间件启动了新 goroutine 并 panic,外层中间件的 defer recover 是捕获不到的——这是最常被忽略的失效点。
- 确保
recover在整个中间件链的最顶层(即第一个注册的中间件),且包裹的是最终next.ServeHTTP()调用 -
recover后必须手动调用w.WriteHeader(statusCode)和json.NewEncoder(w).Encode(...);只log.Print不写响应,客户端会卡在 pending 状态 - 若用 Gin,别同时启用它的
gin.Recovery()和你自己写的RecoverMiddleware,否则可能重复写响应头导致http: superfluous response.WriteHeader call
跨中间件传递 traceID 和错误上下文要用 context.WithValue,别用全局变量
一个请求经过多个中间件(鉴权→限流→日志→业务),错误发生时需要知道“在哪一步、为什么失败”。靠全局变量或包级变量传 traceID 或 reqID 会导致并发污染;必须通过 context.Context 向下透传。
- 入口中间件生成
traceID,用context.WithValue(r.Context(), ctxKeyTraceID, tid)构造新 request - 每个中间件出错时,把
traceID注入AppError字段:&AppError{Code: 429, Message: "rate limited", TraceID: tid} - 日志中间件在
defer里读取r.Context().Value(ctxKeyTraceID),和最终 error 一起打点
真正难的不是写一个 recover 中间件,而是让所有中间件都遵守同一套 error 表达契约:不吞错误、不裸抛 panic、不绕过 context 传元信息。一旦某一层偷偷写了 http.Error 或忽略返回 err,整条链就断了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











