go中间件是需手动组装的函数链,签名必须为func(http.handler) http.handler,且内部须显式调用next.servehttp(w,r);跨中间件传值只能用自定义type的context.key,组合顺序决定执行逻辑。

Go 中间件不是“加个装饰器就生效”的魔法,它是一条必须手动组装、严格签名、显式调用的函数链;写错签名、漏掉 next.ServeHTTP、乱用 context key,都会导致静默失效或 panic。
中间件函数签名必须是 func(http.Handler) http.Handler
这是 net/http 能识别并串联中间件的唯一契约。写成 func(http.ResponseWriter, *http.Request) 或 func(http.HandlerFunc) 看似能编译,但传给 http.Handle 时会被完全忽略——没有报错,请求直接绕过,排查极难。
正确结构有三点缺一不可:
- 参数必须是
next http.Handler(不是http.HandlerFunc) - 返回值必须是
http.Handler(通常用http.HandlerFunc包装) - 内部必须显式调用
next.ServeHTTP(w, r),否则下游永远不执行
典型错误示例:loggingHandler(w http.ResponseWriter, r *http.Request) 是 handler,不是中间件;要把它变成中间件,得包一层:LoggingMiddleware(http.HandlerFunc(yourHandler))。
next.ServeHTTP(w, r) 的位置决定中间件语义
洋葱模型不是框架自动调度的,而是靠你控制 next.ServeHTTP 的时机来实现。位置不同,行为完全不同:
- 放在最前面:前置拦截(如鉴权失败直接
http.Error,不调用next.ServeHTTP) - 放在中间:改写请求后放行(如解析 JWT 后往
r.Context()写用户 ID,再传给下游) - 放在最后:后置处理(如记录耗时、设置响应头),但注意此时响应可能已写出,
w.Header().Set()会 panic - 完全不调用:终止流程(如限流拒绝、CORS 预检通过后直接
w.WriteHeader(204))
调试时可在每层开头加 log.Println("in auth"),看输出顺序验证是否按预期流转。
跨中间件传数据只能用 context.Context,且 key 必须是自定义类型
别用闭包捕获变量,更别用包级全局变量存 request-scoped 数据——Go HTTP server 是并发的,必然引发数据竞争。
安全做法分三步:
- 定义私有 key 类型:
type userIDKey struct{}(避免字符串 key 冲突) - 写入:
ctx := r.Context().WithValue(userIDKey{}, userID),再r = r.WithContext(ctx) - 读取:
if uid, ok := r.Context().Value(userIDKey{}).(string); ok { ... },务必判空和类型断言
常见翻车点:鉴权中间件 A 往 context 写值,日志中间件 B 想读,但 B 注册在 A 外层(即 B(A(handler))),那 B 拿到的还是原始 context,Value() 返回 nil,断言 panic。
组合多个中间件时顺序极易出错
手写嵌套 m1(m2(m3(handler))) 容易把顺序写反;用 pipeline 组合(如 Chain(m1, m2, m3).Then(h))也要注意 fold 方向:它等价于 m1(m2(m3(h))),不是 m3(m2(m1(h)))。
关键原则:
- CORS 中间件必须包在最外层,否则下游提前 return,响应头就丢了
- 鉴权中间件必须在日志之前,否则日志里读不到
ctx.Value(userIDKey) - recover 必须放在最外层中间件,否则 panic 发生时 header 已被上层中间件写出,无法返回 500
最隐蔽的坑是循环注册时用 for i := range ms { h = ms[i](h) },所有闭包捕获同一个 i,最终全指向最后一个中间件——要用 for _, m := range ms { h = m(h) }。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











