vault backup: 2026-04-27 10:10:41
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
---
|
||||
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]]` — 认证/授权中间件实战
|
||||
Reference in New Issue
Block a user