当前位置: 首页 > news >正文

Gin 框架进阶系列(四):中间件机制深入

Gin 框架进阶系列(四):中间件机制深入


中间件本质

Gin 的中间件就是一个 gin.HandlerFunc,和普通路由处理函数签名完全一样:

type HandlerFunc func(*gin.Context)

没有任何特殊接口,能当 handler 的就能当中间件。区别仅在于:中间件通常调用 c.Next() 把控制权交给下一个处理函数,处理完再回来执行后续逻辑。


执行顺序与洋葱模型

func A(c *gin.Context) {fmt.Println("A - 前")c.Next()fmt.Println("A - 后")
}func B(c *gin.Context) {fmt.Println("B - 前")c.Next()fmt.Println("B - 后")
}func handler(c *gin.Context) {fmt.Println("handler")c.JSON(200, gin.H{"msg": "ok"})
}r.Use(A, B)
r.GET("/test", handler)

输出:

A - 前
B - 前
handler
B - 后
A - 后

像洋葱一样,请求从外层进、从外层出。c.Next() 之前是请求阶段,之后是响应阶段。这是理解所有中间件行为的基础。


c.Next() 与 c.Abort() 原理

Gin 内部维护了一个 handler 链(slice)和一个索引 index

// 简化版源码逻辑
func (c *Context) Next() {c.index++for c.index < int8(len(c.handlers)) {c.handlers[c.index](c)c.index++}
}func (c *Context) Abort() {c.index = abortIndex // 设为极大值,后续 handler 不再执行
}

c.Next() 递增索引并依次执行后续 handler。c.Abort() 将索引设为极大值,循环条件不满足,链路直接终止。

关键点:c.Abort() 不会终止当前函数的执行,它只是阻止后续 handler 运行。如果你要立刻返回,必须 return

func AuthMiddleware() gin.HandlerFunc {return func(c *gin.Context) {token := c.GetHeader("Authorization")if token == "" {c.AbortWithStatusJSON(401, gin.H{"msg": "未授权"})return // 不 return 的话,下面的代码还会继续执行}c.Next()}
}

c.AbortWithStatusJSON = c.Abort() + c.JSON(),一步到位。


中间件注册方式

// 全局中间件 —— 对所有路由生效
r.Use(Logger(), Recovery())// 分组中间件 —— 仅对该分组生效
admin := r.Group("/admin")
admin.Use(AuthMiddleware())// 单路由中间件 —— 仅对该路由生效
r.GET("/debug", DebugOnly(), debugHandler)

执行顺序:全局 → 分组 → 单路由,按注册顺序排列。


中间件间传值:c.Set / c.Get

中间件解析出的数据需要传给后续 handler,用 c.Setc.Get

func AuthMiddleware() gin.HandlerFunc {return func(c *gin.Context) {token := c.GetHeader("Authorization")// 假设解析出 userIDuserID, err := parseToken(token)if err != nil {c.AbortWithStatusJSON(401, gin.H{"msg": "token 无效"})return}c.Set("userID", userID)c.Next()}
}// 后续 handler 取值
r.GET("/profile", AuthMiddleware(), func(c *gin.Context) {userID, exists := c.Get("userID")if !exists {c.JSON(500, gin.H{"msg": "userID not found"})return}c.JSON(200, gin.H{"userID": userID})
})

c.Get 返回 any 类型,需要自己断言。可以封装一个辅助函数避免重复代码:

func GetUserID(c *gin.Context) (int64, bool) {v, exists := c.Get("userID")if !exists {return 0, false}id, ok := v.(int64)return id, ok
}

实战:耗时统计中间件

最经典的中间件范例,利用洋葱模型在请求前后各记一次时间:

func Timer() gin.HandlerFunc {return func(c *gin.Context) {start := time.Now()c.Next() // 等所有后续 handler 执行完duration := time.Since(start)status := c.Writer.Status()log.Printf("[%d] %s %s - %v", status, c.Request.Method, c.Request.URL.Path, duration)}
}

c.Next() 之后拿到的 statusduration 是最终结果,因为所有 handler 已经跑完了。


实战:CORS 跨域中间件

func CORS() gin.HandlerFunc {return func(c *gin.Context) {c.Header("Access-Control-Allow-Origin", "*")c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS")c.Header("Access-Control-Allow-Headers", "Content-Type,Authorization")c.Header("Access-Control-Max-Age", "86400")// 预检请求直接返回if c.Request.Method == "OPTIONS" {c.AbortWithStatus(204)return}c.Next()}
}

生产环境不要用 *,应该配置为具体的前端域名。


实战:Recovery 中间件手写版

理解 Gin 内置 Recovery 的原理:

func Recovery() gin.HandlerFunc {return func(c *gin.Context) {defer func() {if err := recover(); err != nil {log.Printf("[PANIC] %v\n%s", err, debug.Stack())c.AbortWithStatusJSON(500, gin.H{"msg": "服务器内部错误"})}}()c.Next()}
}

defer + recover 捕获 panic,防止单个请求崩掉整个进程。debug.Stack() 打印完整调用栈方便排查。


中间件踩坑点

不调用 c.Next() 会怎样? 后续 handler 仍然会执行。因为 Gin 的 for 循环会自动递增索引往下走。c.Next() 的作用是让你能在后续 handler 执行完之后做事。如果你不需要"后置逻辑",不调用 c.Next() 也没问题。

c.Abort() 之后还能写响应吗? 可以。Abort 只阻止后续 handler,不影响当前函数写响应。但要注意不要多次写响应导致 http: superfluous response.WriteHeader call 警告。

异步 goroutine 中不能用原始 c 必须用 c.Copy(),因为请求结束后,Gin 出于性能考取 c 会被复用,直接用会数据竞争:

func AsyncMiddleware() gin.HandlerFunc {return func(c *gin.Context) {cp := c.Copy() // 拷贝一份go func() {time.Sleep(3 * time.Second)log.Printf("异步处理: %s", cp.Request.URL.Path)}()c.Next()}
}

小结

中间件的全部核心就三件事:洋葱模型决定执行顺序,c.Next() 分割前置/后置逻辑,c.Abort() 截断链路。掌握了这三点,无论是鉴权、限流、日志、Recovery,都是同一个套路的不同变体。

下一篇进入Gin + GORM:连接数据库实现 CRUD

http://www.jsqmd.com/news/593169/

相关文章:

  • 论文不同章节降AI策略不同:分章节精准处理的完整教程 - 还在做实验的师兄
  • 告别重复造轮子:用快马AI一键生成嵌入式驱动与协议栈代码
  • 大众点评数据采集终极指南:15分钟搭建智能爬虫系统
  • STM32入门——实时时钟(20)
  • 用Unity 2D碰撞体+Effector,5分钟实现《星露谷物语》式的磁铁吸附效果
  • Kotlin的泛型上下界,枚举 和 字符串资源的区别
  • 告别手动调参:利用快马ai批量对比优化openclaw配置效率提升十倍
  • Roots and the Stars[1]
  • ai辅助开发新体验:让快马ai帮你打造智能win10安装准备助手
  • 降AI前需要做哪些准备:规范操作流程和自查清单 - 还在做实验的师兄
  • 利用快马平台快速原型设计,模拟智能应用控制拦截提示界面
  • 价值:社会对劳动所产生的效用增量形成的局部共识
  • 基于微电网的小信号建模下垂控制稳定性的根轨迹分析
  • MySQL 8.0 与 5.7 全维度深度对比:核心差异、技术演进与新手选型终极指南
  • 基于.NET的Windows窗体编程之WinForms布局简介
  • 云边端数据一致性:分布式事务的实现方法
  • 降AI后怎么验证效果:多平台交叉检测的完整操作方法 - 还在做实验的师兄
  • 告别重复劳动:用快马生成智能安装脚本,一键统一团队openclaw环境配置
  • D8 209.字母异位词 349.两个数组的交集
  • python tarfile
  • AI时代的价值冲击——共识瓦解与转型阵痛
  • 赋能crm新智能:利用快马ai模型实现客户分析与话术建议
  • 降AI工具处理理科公式和图表对比:哪款保留效果最好 - 还在做实验的师兄
  • ESP32 YoRadio架构深度解析:开源网络收音机实现原理与技术配置指南
  • 直流电机双闭环调速控制系统仿真模型 转速电流双闭环PI控制 Matlab/Simulink仿真模型 带报告
  • DisplayLink驱动在Linux系统上的跨发行版兼容性解决方案
  • League Akari 技术解析:从算法原理到实战应用
  • 新手零压力上手:在快马平台生成你的第一个模拟xshell终端
  • 如何用嘎嘎降AI处理文献综述部分:综述专项降AI教程 - 还在做实验的师兄
  • 告别混乱日志:用Logrus Hook为你的Gin应用打造可观测性系统