中间件本质是函数链,即func(http.Handler) http.Handler类型的嵌套函数;必须显式调用next.ServeHTTP,按包裹顺序反向执行,数据共享需通过context,错误和超时须手动处理。

中间件本质是函数链,不是装饰器
Go 的 HTTP 中间件没有语言层面的语法支持,它只是 func(http.Handler) http.Handler 类型的函数。你写的每个中间件都必须接收一个 http.Handler,返回一个新的 http.Handler,并在其 ServeHTTP 方法里决定是否调用下游 handler。这不是“拦截”或“增强”,而是显式构造处理链。
- 错误写法:
func MyMiddleware(http.HandlerFunc)—— 这只能包装单个 handler 函数,无法嵌套或复用 - 正确签名:
func MyMiddleware(next http.Handler) http.Handler - 必须手动调用
next.ServeHTTP(w, r),否则请求就断在中间件里了 - 如果中间件里 panic 了,且没 recover,整个请求会直接 500;标准
http.ServeMux不捕获 panic
顺序决定行为,越靠前的中间件越早执行
中间件的包裹顺序和执行顺序是相反的:最外层中间件最先拿到请求,但要最后才把控制权交给 next。比如 logging(metrics(auth(handler))),执行流是:logging → metrics → auth → handler → auth → metrics → logging。
-
auth在metrics内层,所以它先检查权限,再计时;如果鉴权失败,metrics就不会记录耗时 - 想让日志记录完整耗时?
logging必须包在最外层 - 想让 CORS 头始终存在?
cors中间件得放在最外层,否则下游中间件 return 早了,头就漏了 - 常见翻车点:把
recover放在logging里,但logging包在最外层,结果 panic 发生在auth里,logging根本收不到
Context 传递数据,别用全局变量或闭包存请求级状态
中间件之间共享数据(如用户 ID、请求 ID、DB tx)唯一安全的方式是通过 context.Context。不要用闭包捕获变量、更不要用包级变量存 request-scoped 值——Go 的 HTTP server 是并发的,那样会引发数据竞争。
- 向 context 写值:
ctx := r.Context().WithValue(key, value),注意key要是自定义类型(避免字符串冲突) - 从 context 读值:
value := ctx.Value(key),记得判空和类型断言 - 新建请求对象:
r = r.WithContext(ctx),再传给next.ServeHTTP - 别用
context.Background()或context.TODO()替代 request context —— 它们不带 cancel 和 timeout
别忽略 error handling 和超时控制
标准 net/http 不提供中间件级错误传播机制,也不自动处理超时。如果你的中间件里调用了外部服务(如 Redis、gRPC),必须自己加 context deadline 和错误 fallback。
立即学习“go语言免费学习笔记(深入)”;
- 超时中间件示例中,
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)后,必须defer cancel(),否则 goroutine 泄漏 - 下游 handler 如果用了这个
ctx去调 DB 或 HTTP client,超时会自动传导;但 handler 自己不检查ctx.Err()的话,超时后仍可能继续执行 - 错误中间件不能只 log,还得写响应:
http.Error(w, "Internal Error", http.StatusInternalServerError),否则客户端一直挂起 - 如果中间件里发生不可恢复错误(如 JWT 签名验证失败),应立即 return,**不要**调用
next.ServeHTTP
func TimeoutMiddleware(timeout time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
r = r.WithContext(ctx)
done := make(chan struct{})
go func() {
next.ServeHTTP(w, r)
close(done)
}()
select {
case <-done:
return
case <-ctx.Done():
http.Error(w, "Request timeout", http.StatusGatewayTimeout)
return
}
})
}
}
中间件最难的部分从来不是写法,而是厘清「谁该负责什么」:鉴权中间件不该写日志,日志中间件不该处理 panic,超时中间件不该解析 body。职责切分模糊时,问题会藏在组合后的行为里,而不是某一行代码上。










