go http中间件必须统一为func(http.handler) http.handler签名,这是框架底层强制要求;外层传配置、中层满足签名、内层用http.handlerfunc包裹逻辑并显式调用next.servehttp(w, r)放行。

中间件必须返回 http.Handler 或接收 http.Handler 参数
不是“可以这么写”,而是框架底层调用逻辑强制要求:Go 标准库和所有主流框架(Gin、Echo、GoFrame)都只认 func(http.Handler) http.Handler 这个签名。写成 func()、func(http.ResponseWriter, *http.Request) 或带额外参数的函数,根本进不了请求链。
常见错误现象:panic: interface conversion: interface {} is func(), not gin.HandlerFunc(Gin)或 http.Handle() expects Handler(标准库)。这类 panic 通常发生在注册时,而非运行时,说明类型校验失败得早。
- 正确结构是三层:外层闭包传配置(如
logger *log.Logger),中层符合func(http.Handler) http.Handler,内层用http.HandlerFunc包裹实际逻辑 - 内层必须显式调用
next.ServeHTTP(w, r),漏掉就卡死——这不是可选操作,是放行唯一方式 - 若用 Gin,签名必须是
gin.HandlerFunc,即func(*gin.Context),且不能少括号:r.Use(AuthMiddleware())✅,r.Use(AuthMiddleware)❌(传的是函数地址,不是执行结果)
c.Next() 和 c.Abort() 控制流程走向
在 Gin 中,c.Next() 不是“继续往下走”的语义,而是同步阻塞调用后续所有中间件 + 最终 handler,等它们全部返回后才执行 c.Next() 后面的代码。日志耗时统计、响应头注入都依赖这个特性。
而 c.Abort() 是提前终止整条链,但当前中间件函数体仍会继续执行——所以鉴权失败时,要先写响应(如 c.JSON(401, ...)),再调 c.Abort(),否则下游还会执行。
- 不要在同一个中间件里多次调
c.Next(),会导致 handler 重复执行,可能 panic 或数据错乱 - 想做“前置拦截”(如鉴权),逻辑写在
c.Next()前;想做“后置处理”(如记录耗时),逻辑写在c.Next()后 - 修改请求体(如解密)必须在
c.Next()前完成,并确保新c.Request.Body可重读(用io.NopCloser(bytes.NewBuffer(bodyBytes)));改响应体则必须在c.Next()后操作,否则WriteHeader冲突
中间件顺序错位直接导致功能失效
注册顺序 = 执行顺序,没有隐式调度。比如 gin.Recovery() 必须是第一个 Use(),否则 panic 发生在它之前就逃逸了;AuthMiddleware 必须在所有依赖 c.Get("user") 的中间件之前,否则 c.Set("user", user) 永远没机会被调用。
典型可靠顺序:cors → recovery → auth → logger → router。如果把 logger 放 auth 前,日志里就看不到用户身份;把 auth 放 cors 后,跨域预检请求(OPTIONS)可能被拦住。
- 路由组内注册只影响该组,全局
r.Use()影响全部,别混用导致遗漏 - Gin 的
UseMiddleware()已过时,统一用Use() - 异步 goroutine 中必须用
c.Copy()获取副本上下文,原*gin.Context在 handler 返回后会被回收
读写 c.Request.Body 和 c.Writer 是高频翻车点
c.Request.Body 是一次性流,中间件读一次,业务 handler 再调 c.ShouldBindJSON() 就报 invalid character;c.Writer 是接口,直接断言 *responseWriter 会失败,因为 Gin 内部做了包装。
真正能用的方式只有两种:缓存请求体(小数据可用,大文件慎用内存)、包装响应 writer(自定义结构体实现 Write() 和 WriteHeader(),把输出先写入 bytes.Buffer)。
- 别在中间件里直接调
c.ShouldBindJSON(),轻量检查用c.Request.URL.Query()或 header 即可 - 需要透传 trace_id,必须用
context.WithValue(c.Request.Context(), TraceIDKey, tid),然后c.Request = c.Request.WithContext(...),否则下游拿不到 - 日志里路径字段优先用
c.Request.RequestURI(原始 wire 字节),而不是c.Request.URL.Path(已解码),避免 URL 编码差异导致日志失真
next.ServeHTTP 或 c.Next() 都是一次手动交接,漏掉、错位、重复,都会让整条链行为不可控。最麻烦的从来不是写逻辑,而是控制权交还的时机和上下文传递的完整性。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











