第14章:中间件概念与执行流程
本章目标
学完本章后,你将能够:
- 理解中间件的洋葱模型
- 掌握
c.Next()和c.Abort()的行为 - 理解中间件链的执行顺序
- 亲手画出中间件的执行时序图
前置知识
- 需要先学习:第03章(路由基础)、第04章(路由分组)
14.1 什么是中间件
中间件是在请求到达处理函数之前和响应返回之前执行的代码:
请求 → [中间件1 前置] → [中间件2 前置] → [处理函数] → [中间件2 后置] → [中间件1 后置] → 响应就像剥洋葱——请求从外到内穿过一层层中间件,到达核心处理函数后,响应再从内到外一层层返回:
┌──────────────────────┐
│ Logger 中间件 │
│ ┌────────────────┐ │
│ │ Auth 中间件 │ │
│ │ ┌──────────┐ │ │
│ │ │ Handler │ │ │
│ │ └──────────┘ │ │
│ └────────────────┘ │
└──────────────────────┘
请求 → Logger前 → Auth前 → Handler → Auth后 → Logger后 → 响应14.2 c.Next() 的魔法
go
func MyMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("① 中间件前置:请求到达")
c.Next() // 交给下一个处理器(可能是另一个中间件或最终 Handler)
fmt.Println("③ 中间件后置:响应返回")
}
}
r.GET("/test", MyMiddleware(), func(c *gin.Context) {
fmt.Println("② 处理函数执行")
c.String(200, "done")
})访问 /test 时输出:
① 中间件前置:请求到达
② 处理函数执行
③ 中间件后置:响应返回📌
c.Next()是中间件的核心。没有它,请求就卡住了——不会传给下一个处理器。
14.3 c.Abort() 的中止
go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
fmt.Println("⛔ 认证失败,中止请求")
c.AbortWithStatusJSON(401, gin.H{"error": "未认证"})
return // Abort 后必须 return!
}
c.Next() // 认证通过,继续
}
}c.Abort() 的行为:
- 标记当前请求为"已中止"
- 后续的
c.Next()不会再调用下一个处理器 - 但当前中间件的后置代码仍会执行!
go
func M1(c *gin.Context) {
fmt.Println("M1 前置")
c.Abort() // 中止!不会执行 M2 和 Handler
fmt.Println("M1 后置") // ⚠️ 但这里会执行!
}
func M2(c *gin.Context) {
fmt.Println("M2") // 不会执行
}📌
c.Abort()只是设置了一个"中止标记",不会让当前函数立刻返回。Abort 后必须return才能避免执行后续代码。
14.4 中间件的三种作用范围
go
// 1. 全局中间件:对所有路由生效
r := gin.Default() // Default 自动加了 Logger 和 Recovery
r.Use(MyMiddleware1(), MyMiddleware2())
// 2. 分组中间件:只对分组内的路由生效
api := r.Group("/api")
api.Use(AuthMiddleware())
api.GET("/users", handler) // 需要认证
api.GET("/orders", handler) // 需要认证
// 3. 路由级中间件:只对这一个路由生效
r.GET("/special", M1(), M2(), handler)
// 执行顺序:M1 前置 → M2 前置 → handler → M2 后置 → M1 后置14.5 完整的多中间件示例
go
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func TraceMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
fmt.Printf("[%s] %s %s → %d (%v)\n",
time.Now().Format("15:04:05"),
c.Request.Method,
c.Request.URL.Path,
c.Writer.Status(),
time.Since(start),
)
}
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("Authorization") == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized,
gin.H{"error": "请先登录"})
return
}
c.Next()
}
}
func main() {
r := gin.New()
r.Use(TraceMiddleware()) // 全局:所有请求都打印耗时
api := r.Group("/api")
api.Use(AuthMiddleware()) // 分组:需要认证
{
api.GET("/users", func(c *gin.Context) {
c.JSON(200, gin.H{"users": []string{"张三", "李四"}})
})
}
r.Run(":8080")
}常见错误
错误1:中间件中 c.Next() 之后还在操作响应
go
func BadMiddleware(c *gin.Context) {
c.Next()
c.JSON(200, gin.H{"msg": "ok"}) // ❌ 响应已经写过了!
// [GIN] Headers were already written. Wanted to override status code 200 with 200
}错误2:Abort 后忘 return
go
func Auth(c *gin.Context) {
if token == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "未认证"})
// ❌ 忘记 return!后续代码会执行
}
c.Next() // 认证失败时不应该执行这里
}错误3:全局中间件注册在路由后面
go
// ❌ 中间件在路由后面注册,不会自动对前面的路由生效
r.GET("/test", handler)
r.Use(MyMiddleware()) // 只对后面注册的路由生效
// ✅ 中间件在前,或者用 Default
r.Use(MyMiddleware())
r.GET("/test", handler)本章小结
- 中间件是一个
func(c *gin.Context)函数 c.Next()将控制权交给下一个处理器c.Abort()阻止后续处理器执行- 中间件可全局、分组、路由级三种方式使用
- 中间件执行顺序 = 注册顺序
- 洋葱模型:前置代码从上到下,后置代码从下到上
练习题
- 写一个打印请求耗时的中间件,全局应用。
- 写一个中间件,检查请求头中是否有
X-Token,没有则返回 401。 - 写两个中间件 M1 和 M2,用
fmt.Println观察它们的执行顺序。 - 用
c.Abort()验证:Abort 后置代码是否还会执行? - (思考题)Gin 的中间件模型和 Express.js 的中间件模型有什么相似之处?