This repository has been archived on 2026-05-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
obsidian/BACKEND/GIN/middleware.md
T

368 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [后端, Go, Gin, 中间件, 架构]
create time: 2026-04-27 00:00
---
# 中间件完整机制
## 概述
Gin 的中间件是一个轻量但强大的抽象——本质就是一个 `func(*gin.Context)` 类型的函数,通过 `c.Next()` 决定是否把控制权交给下一个 handler。Gin 的中间件系统有三层作用域,可以精确控制作用范围。
思考题:Gin 的中间件和 Go 标准库 `net/http` 的 `func(http.Handler) http.Handler` 中间件模式有什么本质区别?Gin 的方案更简单还是更灵活?
## 正文
### 1. 中间件的本质
中间件的类型签名就是 `gin.HandlerFunc`:
```go
type HandlerFunc func(*Context)
```
它接收一个 `*gin.Context`,可以:
1. **前置处理**(读取请求、校验、记录日志等)
2. **调用 `c.Next()`**(把控制权交给下一个 handler)
3. **后置处理**(修改响应、收集指标等)
```go
func logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now() // 前置:记录开始时间
c.Next() // ← 必须调用,把控制权交给下一个
// 后置:请求结束后的处理
latency := time.Since(start)
log.Printf("[%d] %s %s — %v", c.Writer.Status(), c.Request.Method, c.Request.URL.Path, latency)
}
}
```
### 2. 三级作用域
Gin 中间件可以在三个层级注册,形成**作用域嵌套**:
```
全局中间件(Engine 级)
├── 分组中间件(RouterGroup 级)
│ ├── 路由中间件(单个路由级)
│ └── 路由中间件
└── 分组中间件
└── 路由中间件
```
**执行顺序:** 全局 → 分组 → 路由 → handler
```go
r := gin.Default()
// 全局中间件 — 所有路由都经过
r.Use(globalMiddleware())
// 分组中间件 — 仅该分组下的路由经过
v1 := r.Group("/api/v1", v1Middleware())
{
// 路由中间件 — 仅此路由经过
v1.GET("/users", routeMiddleware(), listUsers)
v1.GET("/posts", listPosts) // 不经过 routeMiddleware
}
```
**执行链路示意:**
```
请求: GET /api/v1/users
全局中间件(前置处理)
↓
v1 分组中间件(前置处理)
↓
路由中间件(前置处理)
↓
handler: listUsers
↓
路由中间件(c.Next() 返回后的代码)
↓
v1 分组中间件(c.Next() 返回后的代码)
↓
全局中间件(c.Next() 返回后的代码)
```
> **关键理解:** 中间件链是**先入后出**的——`c.Next()` 之前的代码按注册顺序执行,`c.Next()` 之后的代码按**逆序**执行。
```go
// 验证执行顺序
r := gin.Default()
r.Use(func(c *gin.Context) {
fmt.Println("1-before")
c.Next()
fmt.Println("1-after")
})
r.Use(func(c *gin.Context) {
fmt.Println("2-before")
c.Next()
fmt.Println("2-after")
})
r.GET("/test", func(c *gin.Context) {
fmt.Println("handler")
})
// 输出:
// 1-before
// 2-before
// handler
// 2-after
// 1-after
```
思考题:如果中间件 A 中调用了 `c.Abort()`(不调用 `c.Next()`),中间件 B 和 handler 还会执行吗?那 A 中 `c.Abort()` 之后的代码还会执行吗?
### 3. 中间件链的构成
当你调用 `c.Next()` 时,内部执行的是 `c.handlers` 切片:
```go
// gin/context.go
func (c *Context) Next() {
c.index++
for ; c.index < int8(len(c.handlers)); c.index++ {
c.handlers[c.index](c)
}
}
```
`c.handlers` 的来源是 **全局中间件 + 分组中间件 + 路由中间件** 的拼接:
```
全局中间件: [Logger, Recovery]
v1 分组中间件: [V1Middleware]
路由中间件: [RouteMiddleware]
handler: [listUsers]
c.handlers = [Logger, Recovery, V1Middleware, RouteMiddleware, listUsers]
```
注册顺序就是执行顺序:
```go
r := gin.Default() // Logger, Recovery
v1 := r.Group("/api/v1", authMiddleware()) // Logger, Recovery, authMiddleware
{
v1.GET("/users", corsMiddleware(), listUsers) // Logger, Recovery, authMiddleware, corsMiddleware, listUsers
}
```
### 4. 常用中间件实现
#### CORS 中间件
```go
func cors() gin.HandlerFunc {
return func(c *gin.Context) {
origin := c.Request.Header.Get("Origin")
if origin != "" {
c.Header("Access-Control-Allow-Origin", origin)
c.Header("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,PATCH,OPTIONS")
c.Header("Access-Control-Allow-Headers", "Origin,Content-Type,Authorization,X-Token")
c.Header("Access-Control-Max-Age", "86400")
c.Header("Access-Control-Allow-Credentials", "true")
}
// 处理 OPTIONS 预检请求
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
```
#### 认证中间件(JWT)
```go
func jwtAuth() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "missing token"})
c.Abort()
return
}
claims, err := parseJWT(token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid token"})
c.Abort()
return
}
// 把用户信息存入 Context,后续 handler 可直接获取
c.Set("userID", claims.UserID)
c.Set("role", claims.Role)
c.Next()
}
}
```
#### 请求日志(结构化)
```go
func structuredLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
query := c.Request.URL.RawQuery
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
log.WithFields(log.Fields{
"method": c.Request.Method,
"path": path,
"query": query,
"status": status,
"latency_ms": latency.Milliseconds(),
"client_ip": c.ClientIP(),
}).Info("request")
}
}
```
#### 请求 ID 中间件
```go
const requestIDKey = "X-Request-ID"
func requestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader(requestIDKey)
if id == "" {
id = uuid.New().String()
}
c.Set(requestIDKey, id)
c.Header(requestIDKey, id)
c.Next()
}
}
```
> **提问:** 上面的 JWT 中间件中,`c.Set("userID", ...)` 把用户信息存进了 Context。如果认证失败(`c.Abort()`),那这个 `userID` 还会被后面的 handler 读到吗?为什么?
### 5. 中间件中启动 Goroutine 的陷阱
这是 Gin 中间件最常见的坑:**在中间件中启动 goroutine 后,goroutine 可能引用已释放的 Context**。
**错误写法:**
```go
func asyncProcessor() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// 危险!c.Next() 返回后,Context 可能已经被回收
// 但 goroutine 还在运行,可能会 panic
go func() {
log.Printf("处理完成: %s", c.Request.URL.Path) // ← c 可能已被 pool 回收
}()
}
}
```
**正确写法:用 `c.Copy()` 创建独立副本**
```go
func safeAsyncProcessor() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// c.Copy() 创建请求副本,包含独立的 Request 和 Context
// 注意:copy 中的 Writer 是无效的,只能读 Request 和 Context keys
copy := c.Copy()
go func() {
// 安全的异步处理,只读取数据,不能写入响应
log.Printf("异步处理完成: %s", copy.Request.URL.Path)
userID := copy.GetString("userID")
log.Printf("用户 %s 的请求已异步处理", userID)
}()
}
}
```
**`c.Copy()` 的限制:**
| 可以复制的 | 不可以复制的 |
|-----------|-------------|
| `c.Request`(原始请求) | `c.Writer`(ResponseWriter 无法复制) |
| `c.Keys`(已设置的键值对) | `c.Errors`(错误链不能写) |
| `c.Params`(路径参数) | 不能调用 `c.JSON()` 等写响应的方法 |
| `c.ClientIP()` | 不能修改请求体 |
> **核心规则:** `c.Copy()` 出的 goroutine **只能读不能写**。如果需要在异步中写数据,用数据库/队列等持久化方式,不要依赖 Context。
### 6. 跳过中间件
有时不想让特定路由经过某些中间件,有两种方式:
**方式一:将中间件注册到子分组而非全局**
```go
r := gin.Default()
// 只挂载到 /api 分组
api := r.Group("/api", rateLimitMiddleware())
{
api.GET("/public", publicHandler) // 经过 rateLimit
api.GET("/private", privateHandler) // 经过 rateLimit
}
r.GET("/health", healthHandler) // 不经过 rateLimit
```
**方式二:中间件内部判断跳过**
```go
func logging() gin.HandlerFunc {
return func(c *gin.Context) {
// 跳过健康检查端点
if c.Request.URL.Path == "/health" {
c.Next()
return
}
// 正常日志逻辑
start := time.Now()
c.Next()
log.Printf("[%d] %s", c.Writer.Status(), time.Since(start))
}
}
```
### 7. 中间件的常见应用场景
| 场景 | 方案 |
|------|------|
| 跨域处理 | CORS 中间件 |
| 身份认证 | JWT / Session 中间件 |
| 权限控制 | RBAC 中间件 |
| 请求限流 | Token Bucket / 滑动窗口中间件 |
| 请求日志 | 结构化日志中间件 |
| 请求追踪 | RequestID 中间件 |
| 异常恢复 | `gin.Recovery()` |
| 缓存 | 响应缓存中间件 |
| 数据预处理 | 数据注入中间件(如把数据库对象注入 Context) |
思考题:如果需要在多个分组之间共享中间件(比如 `api/v1` 和 `api/v2` 都需要 CORS),是把 CORS 注册到全局好,还是注册到各自分组好?为什么?
## 关联笔记
- `[[GIN/gin-architecture]]` — 中间件链的底层执行机制
- `[[GIN/context-lifecycle]]` — `c.Copy()` 的深拷贝原理
- `[[GIN/session-auth]]` — 认证/授权中间件实战