Gin中间件必须返回gin.HandlerFunc类型函数,签名严格为func(*gin.Context),注册时调用带括号的工厂函数;需用c.Abort()阻断流程,通过c.Set/c.Get传递数据,注意Body只能读一次,全局/分组/单路由中间件作用域不同,日志和recover可辅助调试与兜底。

中间件函数签名必须返回 func(*gin.Context)
Gin 的中间件本质就是一个接收 *gin.Context 并不返回值的函数,但你不能直接写 func(c *gin.Context) { ... } 就完事——它必须被包装成符合 gin.HandlerFunc 类型的函数。常见错误是漏掉返回、或误写成带返回值的函数(比如返回 error 或 bool),导致编译报错:cannot use xxx (type func(*gin.Context) error) as type gin.HandlerFunc。
正确写法是确保签名严格匹配:
func MyMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 你的逻辑
c.Next() // 继续后续处理(可选)
}
}
- 中间件注册时调用的是
MyMiddleware()(带括号),不是MyMiddleware(不带括号) - 如果需要传参(如配置前缀、开关),就在
MyMiddleware()外层函数里接收,闭包捕获后在内部使用 - 不要在中间件里用
return提前退出整个请求流——要用c.Abort()阻断后续 handler;仅用return只是退出当前函数,c.Next()之后的代码仍会执行
如何在中间件里读取和修改请求/响应数据
中间件能访问 c.Request 和 c.Writer,但要注意:直接读 c.Request.Body 会导致后续 handler 读不到(Body 是 io.ReadCloser,只能读一次);直接写 c.Writer.Write() 也容易破坏 Gin 的响应流程(比如状态码、Header 写晚了)。
安全做法:
- 读 Body:用
c.ShouldBindJSON()或c.GetRawData()(后者会重置 Body,适合需多次读取的场景) - 改请求上下文:用
c.Set("key", value)存数据,后续 handler 用c.Get("key")取,比全局 map 更安全 - 改响应:优先用
c.JSON()/c.String()等 Gin 方法;若需拦截原始响应(如日志 body),得用gin.ResponseWriter包装c.Writer,自己实现Write()方法并缓存内容
全局中间件 vs 路由组中间件 vs 单路由中间件的区别
三者注册位置不同,作用域就不同,不是“越靠前越优先”这种线性关系:
-
r.Use(m1, m2):全局,所有路由(包括未分组的)都会经过,顺序按参数从左到右 -
v1 := r.Group("/api/v1", m3):该 Group 下所有子路由额外叠加m3,执行顺序是m1 → m2 → m3 → handler -
r.GET("/ping", m4, pingHandler):仅此路由生效,m4在m3之后、pingHandler之前执行
典型陷阱:把鉴权中间件只加在某个 Group,结果 /login 接口没加,导致未登录也能访问登录页(虽不危险,但逻辑错乱);或者把日志中间件放在单个路由上,漏记大量请求。
调试中间件执行顺序和中断行为
最简单的方法是在每个中间件开头/结尾打日志,但要注意 c.Next() 是同步阻塞调用,日志顺序能直接反映真实执行流。例如:
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
log.Printf("→ %s %s", c.Request.Method, c.Request.URL.Path)
c.Next()
log.Printf("← %s %s %d", c.Request.Method, c.Request.URL.Path, c.Writer.Status())
}
}
- 如果某中间件调用了
c.Abort(),后续中间件和 handler 都不会执行,但已执行过的中间件中c.Next()后的代码仍会运行 -
c.IsAborted()可用于判断是否已被前面中间件中断,适合做清理(比如释放资源),但别依赖它来“补救”响应 - 用
c.FullPath()比c.Request.URL.Path更准,能拿到注册时的路由模式(如/user/:id),方便日志归类
真正容易被忽略的是:中间件里 panic 不会被 Gin 自动 recover(除非你手动加 recovery.Recovery()),一旦 panic 整个请求就 500,且没有堆栈——建议在关键中间件里用 defer + recover 做兜底,或直接依赖 Gin 默认的 recovery 中间件。


















