Go中间件核心契约是func(http.Handler) http.Handler,否则静默失效;需手动处理OPTIONS预检、注意闭包变量复用、用context传递数据、header设置须在WriteHeader前。

Go 中间件不是“写个函数塞进去就行”,核心在于签名必须是 func(http.Handler) http.Handler,漏掉这个契约,中间件就根本进不了调用链——编译不报错,但运行时静默失效,查起来极难定位。
中间件函数签名必须严格匹配 func(http.Handler) http.Handler
这是所有问题的起点。很多新手写成 func(http.ResponseWriter, *http.Request) 或 func(*gin.Context)(混 Gin),结果传给 http.Handle() 时直接被拒绝,错误提示常藏在 IDE 类型检查或构建日志里,不翻源码根本看不到。
- 正确结构:接收一个
http.Handler,返回一个新的http.Handler;内部通过next.ServeHTTP(w, r)显式调用下游 - 错误典型:
func(w http.ResponseWriter, r *http.Request)是 handler 函数,不是 middleware;它不能嵌套,也不能参与链式组装 - Gin 的
gin.HandlerFunc是另一套体系,和原生net/http中间件不兼容——别把两者混着注册
必须手动处理 OPTIONS 预检,否则跨域请求永远卡住
浏览器发 CORS 请求前,会先发一个 OPTIONS 预检。Go 的 net/http 默认不响应它,路由没注册就 404,后续所有 header 设置都白搭。
- 手写中间件时,
if r.Method == "OPTIONS"分支必须存在,并且要w.WriteHeader(204)或return,不能只设 header - 用
gorilla/handlers.CORS更稳:它自动注册预检响应、控制 header 写入时机、校验AllowedOrigins和AllowCredentials是否冲突 - 如果用了 Nginx 反向代理,记得加
proxy_pass_request_headers on;,否则Access-Control-Allow-*头会被默认过滤掉
中间件顺序决定执行流,闭包变量容易被循环复用
中间件是链式包装,m1(m2(m3(h))) 意味着 m1 最先执行前置逻辑、最后执行后置逻辑。而 for 循环注册时,若没正确绑定当前项,所有中间件会共享最后一个闭包值。
立即学习“go语言免费学习笔记(深入)”;
- ❌ 错误写法:
for i := range ms { h = ms[i](h) }——i在循环结束后固定为len(ms),所有ms[i]实际都取最后一项 - ✅ 正确写法:
for _, m := range ms { h = m(h) }—— 每次迭代都拿到独立的m闭包 - 带参数的中间件(如
timeout(30 * time.Second))必须是工厂函数:func(time.Duration) func(http.Handler) http.Handler,确保每次调用生成新闭包
上下文数据传递必须用 context.WithValue,别用全局变量或闭包捕获
多个中间件之间传用户 ID、请求 ID 等,唯一安全方式是基于 r.Context()。全局变量或外层闭包在并发下必然错乱,而且无法按请求隔离。
-
r = r.WithContext(context.WithValue(r.Context(), "user_id", 123))后再调next.ServeHTTP(w, r) - 下游取值时必须做类型断言:
uid, ok := r.Context().Value("user_id").(int),ok不检查会 panic - 键名建议用结构化命名,比如
"auth.user_id",避免第三方中间件键名冲突
最易忽略的点:中间件里调用 w.Header().Set() 必须在 w.WriteHeader() 或任何 w.Write() 之前,否则 header 直接丢弃——这个限制没有错误提示,只能靠经验排查。


















