go中间件必须是func(http.handler) http.handler类型,因http.handle等函数仅接受http.handler接口实现者;需显式调用next.servehttp(w, r)实现洋葱模型,数据传递依赖context.context,顺序决定执行流向,panic恢复须置于最外层。

Go 的中间件不是框架特性,而是函数签名和调用约定的产物;只要满足 func(http.Handler) http.Handler,就能链起来——不依赖 Gin、Echo 或任何第三方库。
为什么必须是 func(http.Handler) http.Handler?
因为 http.ListenAndServe 和 http.Handle 只接受实现了 http.Handler 接口的值,而该接口只认 ServeHTTP(http.ResponseWriter, *http.Request) 方法。中间件要“包住” handler,就必须返回一个新 http.Handler。
- 错误写法:
func(next http.HandlerFunc) http.HandlerFunc—— 返回类型不满足接口,http.Handle("/path", myMW(handler))会编译报错:cannot use myMW(handler) (type http.HandlerFunc) as type http.Handler - 正确写法:内部用
http.HandlerFunc(func(w, r) { ... })包一层,确保返回值是http.Handler - 漏掉
return或返回了错误类型,会导致链断裂——请求卡住、无响应、无日志,调试时只能靠超时发现
怎么手动构造洋葱模型?顺序错了就全乱
洋葱模型不是自动调度的,它完全由函数嵌套结构决定:recovery(logging(auth(handler))) 表示 recovery 最外层,handler 最内层。请求从左往右穿透,响应从右往左返回。
-
auth(logging(handler)):鉴权失败直接拦截,logging根本不执行 -
logging(auth(handler)):哪怕鉴权失败,日志里也能看到method和path(但可能没user_id) -
recovery必须在最外层——否则内层 panic 会穿透到http.Server,整个进程退出 -
body解析类中间件(如读取并重放r.Body)必须放在所有依赖原始Body的中间件之前,否则下游读到空io.ReadCloser
next.ServeHTTP(w, r) 放哪儿,决定了中间件语义
这个调用不是装饰器里的“魔法”,它是你手动控制流程走向的唯一开关。位置不同,行为完全不同:
- 放在最前面:前置拦截(如
if !validToken(r) { http.Error(w, "401", 401); return }) - 放在中间:改写请求后放行(如
r = r.WithContext(context.WithValue(r.Context(), key, value)),再传给next) - 放在最后:后置处理(如记录耗时、设置
Header),但注意:w.Header().Set()在响应已写出后会 panic - 完全不调用:终止流程(如 CORS 预检通过后直接
return)
跨中间件传数据,别碰 *http.Request 和全局变量
*http.Request 是不可变的,不能往里面塞字段;包级变量或闭包捕获外部状态在并发下必然出错。唯一安全方式是 context.Context:
- 写值:
ctx := r.Context().WithValue(key, value),其中key必须是私有 struct 类型(如type userIDKey struct{}),避免字符串冲突 - 读值:
value := r.Context().Value(key),务必做非空判断 + 类型断言 - 传下去:
r = r.WithContext(ctx),再调next.ServeHTTP(w, r) - 别用
context.Background()替代r.Context()——它没有 cancel 和 timeout 信号,会泄漏 goroutine
真正难的不是写单个中间件,而是让多个中间件在并发场景下共享状态又不互相污染——context 是唯一答案,其他都是幻觉。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











