Skip to content

第14章:中间件概念与执行流程 ​

本章目标 ​

学完本章后,你将能够:

  1. 理解中间件的洋葱模型
  2. 掌握 c.Next() 和 c.Abort() 的行为
  3. 理解中间件链的执行顺序
  4. 亲手画出中间件的执行时序图

前置知识 ​

  • 需要先学习:第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() 阻止后续处理器执行
  • 中间件可全局、分组、路由级三种方式使用
  • 中间件执行顺序 = 注册顺序
  • 洋葱模型:前置代码从上到下,后置代码从下到上

练习题 ​

  1. 写一个打印请求耗时的中间件,全局应用。
  2. 写一个中间件,检查请求头中是否有 X-Token,没有则返回 401。
  3. 写两个中间件 M1 和 M2,用 fmt.Println 观察它们的执行顺序。
  4. 用 c.Abort() 验证:Abort 后置代码是否还会执行?
  5. (思考题)Gin 的中间件模型和 Express.js 的中间件模型有什么相似之处?

把零散知识,整理成可以反复走的路。