vault backup: 2026-04-27 22:55:05
This commit is contained in:
@@ -15,7 +15,7 @@ Gin 的本质是一个 **`http.Handler`**——它没有脱离 Go 标准库 `net
|
||||
|
||||
理解 Gin 架构的起点,是认清它和 `net/http` 的边界——Gin 不替代标准库,而是包装和增强它。
|
||||
|
||||
思考题:`gin.Default()` 返回的是一个 `*Engine`,而 `*Engine` 实现了 `http.Handler` 接口。这意味着什么?能不能直接把 Gin Engine 传给标准库的 `http.ListenAndServe`?
|
||||
思考题:`gin.Default()` 返回的是一个 `*Engine`,而 `*Engine` 实现了 `http.Handler` 接口。这意味着什么?能不能直接把 Gin Engine 传给标准库的 `http.ListenAndServe`?(详见 [[1-gin-architecture/engine-handler]])
|
||||
|
||||
## 正文
|
||||
|
||||
@@ -96,7 +96,7 @@ type Context struct {
|
||||
|
||||
**重点理解:** 每个请求都会创建一个独立的 `*gin.Context`,对象会复用(sync.Pool),但 `keys` map 是每个请求隔离的。
|
||||
|
||||
> **提问:** `gin.Context` 被 `sync.Pool` 复用,那如果在 handler 中把 `c` 保存到全局变量里,下次请求读到的是什么?
|
||||
> **提问:** `gin.Context` 被 `sync.Pool` 复用,那如果在 handler 中把 `c` 保存到全局变量里,下次请求读到的是什么?(详见 [[1-gin-architecture/context-pool]])
|
||||
|
||||
### 2. 请求生命周期(从 HTTP 到 Handler)
|
||||
|
||||
@@ -284,4 +284,5 @@ graph LR
|
||||
- `[[GIN/routing]]` — 路由匹配算法深入(Radix Tree)
|
||||
- `[[GIN/middleware]]` — 中间件机制与执行顺序
|
||||
- `[[GIN/context-lifecycle]]` — Context 的底层设计与陷阱
|
||||
- `[[1-gin-architecture/engine-handler]]` — Gin 与 http.Handler 的关系
|
||||
- `[[Go 后端基础]]` — Gin 基础用法入门
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
tags:
|
||||
- 后端
|
||||
- Go
|
||||
- Gin
|
||||
- 并发
|
||||
create time: 2026-04-27 10:05
|
||||
---
|
||||
|
||||
# Context 池化与常见陷阱
|
||||
|
||||
## 概述
|
||||
|
||||
`gin.Context` 通过 `sync.Pool` 复用以减少 GC 压力,这带来了性能红利,也埋下了并发陷阱。本文深入剖析池化机制、保存 Context 引用会引发的数据错乱问题,以及如何正确使用。
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. Context 的复用流程
|
||||
|
||||
Gin 在每次请求的完整生命周期中复用同一个 `*gin.Context` 对象:
|
||||
|
||||
```go
|
||||
// gin/engine.go — ServeHTTP 简化版
|
||||
func (engine *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c := engine.getContext() // 从 sync.Pool 取 Context(可能是旧的)
|
||||
c.Reset() // 清空所有字段,洗白板
|
||||
c.Request = r
|
||||
c.writermem.reset(w)
|
||||
|
||||
handlers, params, _ := engine.tree.match(r.URL.Path, r.Method)
|
||||
c.handlers = handlers
|
||||
c.params = params
|
||||
c.index = -1
|
||||
|
||||
c.Next() // 执行中间件链 + 用户 handler
|
||||
|
||||
engine.freeContext(c) // 归还到 sync.Pool,等待下次复用
|
||||
}
|
||||
```
|
||||
|
||||
关键链路:**从 pool 取 → Reset 清空 → 填充新数据 → 执行完毕 → 归还 pool**。
|
||||
|
||||
这意味着 `sync.Pool` 里存的是**被洗过白板的对象**,不是干净的新对象。
|
||||
|
||||
### 2. 陷阱:保存 c 到全局变量
|
||||
|
||||
如果在 handler 中把 `c` 保存到全局变量,**下次请求读到的是新请求的上下文**——因为对象被复用,字段被覆盖。
|
||||
|
||||
#### 错误示范
|
||||
|
||||
```go
|
||||
var globalC *gin.Context
|
||||
|
||||
func SaveHandler(c *gin.Context) {
|
||||
globalC = c // 保存的是对象引用,不是副本
|
||||
c.String(200, "saved")
|
||||
}
|
||||
|
||||
func GetHandler(c *gin.Context) {
|
||||
// 期望读到上次请求的数据,实际读到的是某个并发请求的数据!
|
||||
req := globalC.Request
|
||||
fmt.Println(req.URL.Path) // ← 可能是任意请求的路径
|
||||
}
|
||||
```
|
||||
|
||||
#### 字段被覆盖对照表
|
||||
|
||||
| 字段 | 请求 A 写入 | 请求 B 到来后(`c.Reset()`) |
|
||||
|------|-----------|--------------------------|
|
||||
| `globalC.Request` | A 的 `*http.Request` | **变成 B 的 Request** |
|
||||
| `globalC.keys` | `map[string]any{"user": "alice"}` | **被置为 nil,请求 B 重新 Set 后恢复** |
|
||||
| `globalC.Params` | A 的路径参数 | **变成 B 的路径参数** |
|
||||
| `globalC.writermem` | A 的响应缓冲 | **被重置,A 的已写入响应数据丢失** |
|
||||
|
||||
#### 并发场景下的灾难
|
||||
|
||||
当多个并发请求到来时,问题会进一步恶化:
|
||||
|
||||
```
|
||||
时间线:
|
||||
T1: 请求 A 进入 → globalC = ctx(指向 pool 中的对象 X)
|
||||
T2: 请求 B 进入 → ctx 归还 pool → 新请求 C 从 pool 取出同一个对象 X
|
||||
T3: 请求 C 的 c.Reset() → globalC 指向的对象被清空 → 请求 A 的数据丢失!
|
||||
T4: GetHandler 读取 globalC → 读到的是请求 C 的数据,不是 A 的
|
||||
```
|
||||
|
||||
**这不是简单的数据竞争,而是数据错乱**——你读到的既不是上次请求的数据,也不是当前请求的数据,而是**某个并发请求正在使用的数据**。
|
||||
|
||||
### 3. 正确做法
|
||||
|
||||
#### 方案一:拷贝数据而非保存引用
|
||||
|
||||
```go
|
||||
var uid string
|
||||
|
||||
func SaveHandler(c *gin.Context) {
|
||||
uid = c.GetString("user_id") // 拷贝值,不是保存 c
|
||||
}
|
||||
|
||||
func GetHandler(c *gin.Context) {
|
||||
_ = uid // 安全:拷贝的是基本类型值
|
||||
}
|
||||
```
|
||||
|
||||
#### 方案二:c.Set + c.Copy(推荐用于 goroutine)
|
||||
|
||||
如果需要在异步 goroutine 中使用请求数据:
|
||||
|
||||
```go
|
||||
func MyHandler(c *gin.Context) {
|
||||
c.Set("user_id", "123")
|
||||
|
||||
// ✅ 正确:c.Copy() 创建独立副本
|
||||
go func() {
|
||||
c2 := c.Copy() // 深拷贝 Context,keys 被浅拷贝到新 map
|
||||
time.Sleep(1 * time.Second)
|
||||
// c2 在这里是安全的,不受 pool 复用影响
|
||||
fmt.Println(c2.GetString("user_id")) // "123"
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
`c.Copy()` 的核心实现:
|
||||
|
||||
```go
|
||||
func (c *Context) Copy() *Context {
|
||||
copy := &Context{
|
||||
writermem: c.writermem.Clone(), // 克隆响应缓冲
|
||||
Params: c.Params, // Params 是 []Param,线程安全
|
||||
engine: c.engine,
|
||||
}
|
||||
// 浅拷贝 keys map
|
||||
if c.keys != nil {
|
||||
copy.keys = copyMap(c.keys) // 创建新 map,拷贝所有键值对
|
||||
}
|
||||
copy.Request = c.Request // *http.Request 本身是只读的,共享安全
|
||||
return copy
|
||||
}
|
||||
```
|
||||
|
||||
> **重点理解**:`c.Copy()` 做浅拷贝——`keys` map 本身是新的(键值对也是副本),但 `Request` 共享同一个 `*http.Request` 指针(标准库保证了 handler 执行期间 Request 不会被修改)。
|
||||
|
||||
### 4. 设计原则总结
|
||||
|
||||
```
|
||||
Context 池化 = 性能红利 + 并发陷阱
|
||||
|
||||
✅ 安全:
|
||||
- 在 handler 同步代码中使用 c
|
||||
- 传给 goroutine 前先用 c.Copy()
|
||||
- 只拷贝需要的数据值(c.GetString 等)
|
||||
|
||||
❌ 危险:
|
||||
- 保存 c 的引用到全局/包级变量
|
||||
- 在 handler 中启动 goroutine 直接使用 c
|
||||
- 跨请求传递 c 的引用
|
||||
```
|
||||
|
||||
### 5. 思考题
|
||||
|
||||
1. `c.Copy()` 是深拷贝还是浅拷贝?如果 `c.Set("user", userObj)` 存了一个指针类型,拷贝后两个 Context 里的 `userObj` 指向同一个对象吗?修改其中一个会互相影响吗?
|
||||
2. `sync.Pool` 的 Get/Put 是线程安全的,那从 pool 取出的 Context 在并发请求场景下,有没有可能两个请求同时拿到同一个对象?为什么不会?
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[1-gin-architecture]] — Gin 整体架构
|
||||
- [[middleware]] — 中间件机制
|
||||
- [[engine-handler]] — Gin 与 http.Handler 的关系
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
tags:
|
||||
- 后端
|
||||
- Go
|
||||
- Gin
|
||||
- 标准库
|
||||
create time: 2026-04-27 10:05
|
||||
---
|
||||
|
||||
# Gin 与 http.Handler 的关系
|
||||
|
||||
## 概述
|
||||
|
||||
`*gin.Engine` 实现了 `http.Handler` 接口,这使得 Gin 能够无缝接入 Go 标准库 `net/http` 生态。本文深入剖析这一设计背后的原理、意义和实际用法。
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. 接口匹配:为什么能传?
|
||||
|
||||
一切源于一个简单的类型匹配:
|
||||
|
||||
```go
|
||||
// 标准库定义
|
||||
type Handler interface {
|
||||
ServeHTTP(ResponseWriter, *Request)
|
||||
}
|
||||
|
||||
// Gin 实现了它
|
||||
func (engine *Engine) ServeHTTP(w ResponseWriter, r *Request)
|
||||
```
|
||||
|
||||
`*gin.Engine` 的 `ServeHTTP` 方法签名与 `http.Handler` 完全吻合,所以赋值关系天然成立:
|
||||
|
||||
```go
|
||||
var _ http.Handler = (*gin.Engine)(nil) // 编译期确认实现
|
||||
```
|
||||
|
||||
### 2. 三种启动方式,底层都一样
|
||||
|
||||
| 方式 | 代码 | 本质 |
|
||||
|------|------|------|
|
||||
| `r.Run()` | 框架封装 | `http.ListenAndServe(":8080", r)` |
|
||||
| `http.ListenAndServe` | 标准库 | 直接传入 `*gin.Engine` |
|
||||
| `http.Server{Handler: r}` | 高级控制 | 可自定义 ReadTimeout、TLS 等 |
|
||||
|
||||
**方式一:`r.Run()`(最常用)**
|
||||
|
||||
```go
|
||||
r := gin.Default()
|
||||
r.Run(":8080") // 默认 :8080,可省略端口
|
||||
```
|
||||
|
||||
**方式二:标准库直接启动**
|
||||
|
||||
```go
|
||||
r := gin.Default()
|
||||
http.ListenAndServe(":8080", r) // 完全等价
|
||||
```
|
||||
|
||||
**方式三:`http.Server`(生产推荐)**
|
||||
|
||||
```go
|
||||
r := gin.Default()
|
||||
srv := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: r, // 把 Engine 作为 Handler 传入
|
||||
// 可自定义超时、TLS 等
|
||||
ReadTimeout: 5 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
}
|
||||
srv.ListenAndServe()
|
||||
```
|
||||
|
||||
> **提问:** 如果用 `http.Server` 配置了 `ReadTimeout`,这个超时作用在 Gin 的哪个阶段?如果请求体很大且读取很慢,Gin 的 `c.Request` 还能拿到完整的 body 吗?
|
||||
|
||||
### 3. 这意味着什么?
|
||||
|
||||
三层含义:
|
||||
|
||||
**① Gin 不是另起炉灶**
|
||||
|
||||
Gin 没有绕开 `net/http` 自己监听端口,而是成为标准的 `http.Handler`,站在标准库的肩膀上做增强。
|
||||
|
||||
**② 标准库知识完全通用**
|
||||
|
||||
```
|
||||
标准库知识 → 完全适用于 Gin
|
||||
Gin 知识 → 不等同于懂标准库
|
||||
```
|
||||
|
||||
你在标准库里学的中间件模式(`http.Handler` 包装 `http.Handler`),可以直接套用到 Gin 上。反过来则不成立——你会 Gin 不代表你懂 `net/http`,因为 Gin 隐藏了底层的细节。
|
||||
|
||||
**③ 可以与标准库组件自由组合**
|
||||
|
||||
```go
|
||||
r := gin.Default()
|
||||
|
||||
// 标准库中间件也可以用在 Gin 上
|
||||
r.Use(func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
log.Printf("[%s] %s", req.Method, req.URL.Path)
|
||||
next.ServeHTTP(w, req)
|
||||
})
|
||||
})
|
||||
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"message": "pong"})
|
||||
})
|
||||
r.Run()
|
||||
```
|
||||
|
||||
> **提问:** 标准库中间件接收的是 `http.Handler`,而 Gin 中间件接收的是 `*gin.Context`。两者能不能混用?混用时的执行顺序是怎样的?
|
||||
|
||||
### 4. Gin 的 ServeHTTP 做了什么
|
||||
|
||||
当 `ServeHTTP` 被标准库调用时,它执行了完整的请求处理流程:
|
||||
|
||||
```go
|
||||
// gin/engine.go — 简化版
|
||||
func (engine *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// 1. 从 sync.Pool 取 Context
|
||||
c := engine.getContext()
|
||||
c.Reset()
|
||||
c.Request = r
|
||||
c.writermem.reset(w)
|
||||
|
||||
// 2. 按方法找到对应的 Radix Tree,匹配路径
|
||||
handlers, params, _ := engine.tree.match(r.URL.Path, r.Method)
|
||||
|
||||
// 3. 分配 handler 链
|
||||
c.handlers = handlers
|
||||
c.params = params
|
||||
c.index = -1
|
||||
|
||||
// 4. 执行中间件链
|
||||
c.Next()
|
||||
|
||||
// 5. 归还 Context 到 pool
|
||||
engine.freeContext(c)
|
||||
}
|
||||
```
|
||||
|
||||
关键链条:**标准库调用 `ServeHTTP` → Gin 做路由匹配 → 执行中间件链 → 写入响应**。
|
||||
|
||||
> **思考题:** `ServeHTTP` 的 `w` 参数是标准库的 `http.ResponseWriter`,但 Gin 内部用 `responseWriter` 做了包装(加了缓冲)。这意味着响应数据是先写入了 Gin 的缓冲区,再由 `responseWriter` 统一 flush 到标准库的 `w`。这种设计有什么好处?如果 handler 里直接调用 `w.WriteHeader(500)`,Gin 的缓冲机制还能正常工作吗?
|
||||
|
||||
### 5. 核心结论
|
||||
|
||||
Gin 不是另一个 HTTP 框架,它是 `net/http` 的**增强层**:
|
||||
|
||||
```
|
||||
net/http 提供 ──┬── Server、Listener、ResponseWriter
|
||||
├── Request、Response、Header
|
||||
└── ServeMux 路由
|
||||
|
||||
Gin 替代/增强 ──┬── ServeMux → Radix Tree 路由
|
||||
├── ResponseWriter → buffered writer
|
||||
├── Request → gin.Context 包装
|
||||
└── Handler → 中间件链 + 绑定 + 渲染
|
||||
```
|
||||
|
||||
所有标准库的知识完全适用于 Gin,反之则不然——你会 Gin 不代表你懂 `net/http`。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[1-gin-architecture]] — Gin 整体架构
|
||||
- [[middleware]] — 中间件机制
|
||||
- [[context-lifecycle]] — Context 的底层设计
|
||||
@@ -11,6 +11,8 @@ Gin 的路由系统是整个框架的性能核心——它用 **Radix Tree(基
|
||||
|
||||
思考题:Gin 的路由匹配比 `http.ServeMux` 快多少?为什么?(提示:思考标准库 ServeMux 匹配一个 URL 需要遍历什么)
|
||||
|
||||
> 详细解答见:[[2-routing/2-routing-complexity-comparison]]
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. 路由注册
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, 路由, 性能]
|
||||
create time: 2026-04-27 00:00
|
||||
---
|
||||
|
||||
# 路由匹配复杂度对比:Gin vs http.ServeMux
|
||||
|
||||
## 概述
|
||||
|
||||
对比 Gin 的 Radix Tree 路由匹配与 Go 标准库 `http.ServeMux` 的匹配机制,从数据结构、算法复杂度到实际性能差异进行全面分析。
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. ServeMux 的匹配机制
|
||||
|
||||
`http.ServeMux` 内部维护一个 `map[string]pattern` 映射,匹配一个 URL 时需要:
|
||||
|
||||
1. **遍历所有已注册的路由**(按最长前缀匹配规则)
|
||||
2. 对每条路由做一次**完整的路径字符串比较**
|
||||
3. 时间复杂度 **O(R × L)**,其中 **R = 路由总数**,**L = URL 长度**
|
||||
|
||||
> 补充细节:`ServeMux` 支持两种匹配模式:前缀匹配(`/prefix/`)和精确匹配。前缀匹配需要从所有可能的前缀中筛选出最长的候选者,因此会遍历多个条目,而不是 O(1) 的哈希查找。
|
||||
|
||||
### 2. Gin Radix Tree 的匹配机制
|
||||
|
||||
Gin 的路由组织为一棵压缩前缀树(Radix Tree),匹配过程:
|
||||
|
||||
1. 从根节点开始,沿着 URL 的字符**逐段向下走**
|
||||
2. 每段匹配利用前缀共享,一次比较就决定走向
|
||||
3. 时间复杂度 **O(D)**,其中 **D = URL 深度(路径段数)**,通常只有 3-5 层
|
||||
|
||||
> 与路由总数 **R 无关**。10 条路由和 10000 条路由,匹配 `/api/v1/users/5` 的步数几乎一样。
|
||||
|
||||
### 3. 具体差距
|
||||
|
||||
| 场景 | ServeMux | Gin (Radix Tree) |
|
||||
|------|----------|-----------------|
|
||||
| 10 条路由 | ~10 次完整比较 | ~4 步 |
|
||||
| 100 条路由 | ~100 次 | ~4 步 |
|
||||
| 1000 条路由 | ~1000 次 | ~4 步 |
|
||||
| 10000 条路由 | ~10000 次 | ~4 步 |
|
||||
|
||||
**数量级差异**:当路由数增长时,ServeMux 线性增长,Gin 基本保持不变。在路由较多的场景下,Gin 的匹配速度通常是 ServeMux 的 **10~100 倍甚至更多**。
|
||||
|
||||
### 4. 根本原因:数据结构差异
|
||||
|
||||
- **ServeMux 用的是哈希表**:`map[string]Handler` 虽然对精确匹配是 O(1),但 HTTP 路由还需要处理前缀匹配(`/api/...`),这退化为遍历比较。哈希表丢失了"路径前缀"的结构信息。
|
||||
|
||||
- **Gin 用的是 Radix Tree**:天然保留路径前缀结构,匹配时走树不走表,每一步都能利用前缀信息直接跳到下一段,不需要回溯或比较无关路由。
|
||||
|
||||
**一句话总结**:ServeMux 是在一叠卡片里逐张翻,Gin 是在字典里按部首和拼音直接定位。
|
||||
|
||||
### 5. 可视化对比
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph S ["ServeMux:遍历比较"]
|
||||
S1["遍历路由 #1: /users/list"] --> S2["遍历路由 #2: /users/:id"]
|
||||
S2 --> S3["遍历路由 #3: /users/*path"]
|
||||
S3 --> S4["..."]
|
||||
S4 --> S5["遍历路由 #N"]
|
||||
end
|
||||
|
||||
subgraph G ["Radix Tree:树形定位"]
|
||||
G1["根节点 /"] --> G2["users"]
|
||||
G2 --> G3{下一步?}
|
||||
G3 -->|"list"| G4["list ✓"]
|
||||
G3 -->|"数字"| G5[":id ✓"]
|
||||
G3 -->|"其他"| G6["*path ✓"]
|
||||
end
|
||||
|
||||
classDef S fill:#ffebee,stroke:#c62828
|
||||
classDef G fill:#e8f5e9,stroke:#2e7d32
|
||||
class S,S1,S2,S3,S4,S5 S
|
||||
class G,G1,G2,G3,G4,G5,G6 G
|
||||
```
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- `[[2-routing]]` — Gin 路由匹配与分组机制
|
||||
@@ -0,0 +1,130 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, 中间件, 架构]
|
||||
create time: 2026-04-27 12:51
|
||||
---
|
||||
|
||||
# c.Abort() 终止机制
|
||||
|
||||
## 概述
|
||||
|
||||
本文档解答中间件文档中的思考题:当中间件 A 调用 `c.Abort()` 后,后续中间件、handler 以及 A 自身代码的执行情况。
|
||||
|
||||
## 正文
|
||||
|
||||
### 思考题
|
||||
|
||||
如果中间件 A 中调用了 `c.Abort()`(不调用 `c.Next()`),中间件 B 和 handler 还会执行吗?那 A 中 `c.Abort()` 之后的代码还会执行吗?
|
||||
|
||||
**两个问题的答案都是「不会执行」。**
|
||||
|
||||
### 1. 中间件 B 和 handler 还会执行吗?
|
||||
|
||||
**不会。** `c.Abort()` 会立即终止整个中间件链。
|
||||
|
||||
要理解原因,需要看 `c.Next()` 的内部实现——它按索引遍历 `c.handlers` 切片:
|
||||
|
||||
```go
|
||||
func (c *Context) Next() {
|
||||
c.index++
|
||||
for ; c.index < int8(len(c.handlers)); c.index++ {
|
||||
c.handlers[c.index](c)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`c.Abort()` 内部做了两件事:
|
||||
|
||||
1. 设置 `c.IsAborted = true` 标记
|
||||
2. **将 `c.index` 设置为 handler 链的长度**,使得 `c.Next()` 的 for 循环条件立即不满足
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph 正常流程
|
||||
N1["c.index = 0"] --> N2["c.handlers[0] 执行"] --> N3["c.Next() → c.index++"] --> N4["c.handlers[1] 执行"] --> N5["继续..."]
|
||||
end
|
||||
|
||||
subgraph Abort 流程
|
||||
A1["c.index = 0"] --> A2["c.handlers[0] 执行"] --> A3["c.Abort()"] --> A4["c.index 设为链长度"] --> A5["c.Next() → for 条件不满足,直接退出"]
|
||||
end
|
||||
|
||||
style A4 fill:#FF6B6B
|
||||
style A5 fill:#FFD700
|
||||
```
|
||||
|
||||
所以中间件 B 和 handler **都会被跳过**。
|
||||
|
||||
### 2. A 中 c.Abort() 之后的代码还会执行吗?
|
||||
|
||||
**不应该执行。** 因为 `c.Abort()` 调用之后紧接着就是 `return`:
|
||||
|
||||
```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 // ← 立即返回,后面的代码不会执行
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 忘记 return 的后果
|
||||
|
||||
如果忘记写 `return`,Go 语法层面 `c.Abort()` 之后的代码**仍然会执行**,但后续再调用 `c.Next()` 时中间件链会被终止。这会导致**后置处理逻辑意外执行**,是典型的 Bug:
|
||||
|
||||
```go
|
||||
// 危险写法!
|
||||
func badMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
|
||||
if someCondition {
|
||||
c.Abort()
|
||||
// 忘记 return → 继续执行下面代码
|
||||
}
|
||||
|
||||
c.Next() // ← 由于 c.index 已被设为链长度,这里不会进入下一个中间件
|
||||
// 但「下面的」后置代码会执行!
|
||||
|
||||
// 这段后置代码在不应该执行的时候被执行了
|
||||
log.Printf("耗时: %v", time.Since(start))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. AbortWithStatus
|
||||
|
||||
Gin 还提供了 `c.AbortWithStatus(code)`,它在调用 `c.Abort()` 的同时写入响应状态码(body 为空),适合预检请求等场景:
|
||||
|
||||
```go
|
||||
func cors() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return // 同样需要 return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
也有 `c.AbortWithStatusJSON(code, json)`,在 Abort 的同时写入 JSON 响应体。
|
||||
|
||||
## 总结
|
||||
|
||||
| 调用 `c.Abort()` 后 | 结果 |
|
||||
|---|---|
|
||||
| 后续中间件(B、C…) | ❌ 不执行 |
|
||||
| Handler | ❌ 不执行 |
|
||||
| 后续中间件链中的 `c.Next()` | ❌ 不会进入下一个 |
|
||||
| A 中 `c.Abort()` 之后的代码 | ❌ 不应该执行(必须紧跟 `return`) |
|
||||
|
||||
**核心规则:`c.Abort()` 之后必须紧跟 `return`。** 这是 Gin 中间件写作的铁律。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[GIN/3-middleware]] — 中间件完整机制,包含 Abort 的思考题
|
||||
- [[GIN/4-context-lifecycle]] — Context 的深拷贝原理
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, 中间件, 架构]
|
||||
create time: 2026-04-27 00:00
|
||||
create time: 2026-04-27 12:51
|
||||
---
|
||||
|
||||
# 中间件完整机制
|
||||
|
||||
## 概述
|
||||
|
||||
Gin 的中间件是一个轻量但强大的抽象——本质就是一个 `func(*gin.Context)` 类型的函数,通过 `c.Next()` 决定是否把控制权交给下一个 handler。Gin 的中间件系统有三层作用域,可以精确控制作用范围。
|
||||
本文档系统梳理 Gin 中间件的完整机制:从类型签名、三级作用域嵌套、执行顺序,到常见实战场景(CORS、JWT 认证、结构化日志等),以及最容易踩坑的 `c.Copy()` 异步 Goroutine 问题。
|
||||
|
||||
思考题:Gin 的中间件和 Go 标准库 `net/http` 的 `func(http.Handler) http.Handler` 中间件模式有什么本质区别?Gin 的方案更简单还是更灵活?
|
||||
思考题:Gin 的中间件和 Go 标准库 `net/http` 的 `func(http.Handler) http.Handler` 模式有什么本质区别?Gin 的方案更简单还是更灵活?详见 [[GIN/3-middleware/gin-vs-std]]。
|
||||
|
||||
## 正文
|
||||
|
||||
@@ -21,17 +21,17 @@ Gin 的中间件是一个轻量但强大的抽象——本质就是一个 `func(
|
||||
type HandlerFunc func(*Context)
|
||||
```
|
||||
|
||||
它接收一个 `*gin.Context`,可以:
|
||||
1. **前置处理**(读取请求、校验、记录日志等)
|
||||
2. **调用 `c.Next()`**(把控制权交给下一个 handler)
|
||||
3. **后置处理**(修改响应、收集指标等)
|
||||
它接收一个 `*gin.Context`,执行三个阶段:
|
||||
1. **前置处理** — 读取请求、校验、记录日志等
|
||||
2. **调用 `c.Next()`** — 把控制权交给下一个 handler
|
||||
3. **后置处理** — 修改响应、收集指标等
|
||||
|
||||
```go
|
||||
func logger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now() // 前置:记录开始时间
|
||||
start := time.Now() // 前置:记录开始时间
|
||||
|
||||
c.Next() // ← 必须调用,把控制权交给下一个
|
||||
c.Next() // ← 必须调用,把控制权交给下一个
|
||||
|
||||
// 后置:请求结束后的处理
|
||||
latency := time.Since(start)
|
||||
@@ -44,13 +44,13 @@ func logger() gin.HandlerFunc {
|
||||
|
||||
Gin 中间件可以在三个层级注册,形成**作用域嵌套**:
|
||||
|
||||
```
|
||||
全局中间件(Engine 级)
|
||||
├── 分组中间件(RouterGroup 级)
|
||||
│ ├── 路由中间件(单个路由级)
|
||||
│ └── 路由中间件
|
||||
└── 分组中间件
|
||||
└── 路由中间件
|
||||
```mermaid
|
||||
graph TD
|
||||
Engine["Engine 级<br/>全局中间件"] --> v1["RouterGroup /api/v1<br/>分组中间件"]
|
||||
Engine --> v2["RouterGroup /api/v2<br/>分组中间件"]
|
||||
v1 --> u1["GET /users<br/>路由中间件"]
|
||||
v1 --> p1["GET /posts<br/>路由中间件"]
|
||||
v2 --> u2["GET /users<br/>路由中间件"]
|
||||
```
|
||||
|
||||
**执行顺序:** 全局 → 分组 → 路由 → handler
|
||||
@@ -70,27 +70,30 @@ v1 := r.Group("/api/v1", v1Middleware())
|
||||
}
|
||||
```
|
||||
|
||||
**执行链路示意:**
|
||||
**执行链路示意(先入后出):**
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph 前置处理["◀ 前置处理(按注册顺序)"]
|
||||
A1["全局中间件"]
|
||||
A2["v1 分组中间件"]
|
||||
A3["路由中间件"]
|
||||
end
|
||||
subgraph 后置处理["▶ 后置处理(按逆序)"]
|
||||
B3["路由中间件"]
|
||||
B2["v1 分组中间件"]
|
||||
B1["全局中间件"]
|
||||
end
|
||||
A1 --> A2 --> A3 --> H["handler: listUsers"] --> B3 --> B2 --> B1
|
||||
|
||||
style A1 fill:#90EE90
|
||||
style A2 fill:#90EE90
|
||||
style A3 fill:#90EE90
|
||||
style B1 fill:#FFB6C1
|
||||
style B2 fill:#FFB6C1
|
||||
style B3 fill:#FFB6C1
|
||||
style H fill:#FFD700
|
||||
```
|
||||
请求: GET /api/v1/users
|
||||
|
||||
全局中间件(前置处理)
|
||||
↓
|
||||
v1 分组中间件(前置处理)
|
||||
↓
|
||||
路由中间件(前置处理)
|
||||
↓
|
||||
handler: listUsers
|
||||
↓
|
||||
路由中间件(c.Next() 返回后的代码)
|
||||
↓
|
||||
v1 分组中间件(c.Next() 返回后的代码)
|
||||
↓
|
||||
全局中间件(c.Next() 返回后的代码)
|
||||
```
|
||||
|
||||
> **关键理解:** 中间件链是**先入后出**的——`c.Next()` 之前的代码按注册顺序执行,`c.Next()` 之后的代码按**逆序**执行。
|
||||
|
||||
```go
|
||||
// 验证执行顺序
|
||||
@@ -120,7 +123,7 @@ r.GET("/test", func(c *gin.Context) {
|
||||
// 1-after
|
||||
```
|
||||
|
||||
思考题:如果中间件 A 中调用了 `c.Abort()`(不调用 `c.Next()`),中间件 B 和 handler 还会执行吗?那 A 中 `c.Abort()` 之后的代码还会执行吗?
|
||||
思考题:如果中间件 A 中调用了 `c.Abort()`(不调用 `c.Next()`),中间件 B 和 handler 还会执行吗?那 A 中 `c.Abort()` 之后的代码还会执行吗?详见 [[GIN/3-middleware-abort]]。
|
||||
|
||||
### 3. 中间件链的构成
|
||||
|
||||
@@ -136,15 +139,31 @@ func (c *Context) Next() {
|
||||
}
|
||||
```
|
||||
|
||||
`c.handlers` 的来源是 **全局中间件 + 分组中间件 + 路由中间件** 的拼接:
|
||||
`c.handlers` 的来源是 **全局 + 分组 + 路由** 中间件的拼接:
|
||||
|
||||
```
|
||||
全局中间件: [Logger, Recovery]
|
||||
v1 分组中间件: [V1Middleware]
|
||||
路由中间件: [RouteMiddleware]
|
||||
handler: [listUsers]
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph handlers["c.handlers 拼接结果"]
|
||||
H1["Logger"]
|
||||
H2["Recovery"]
|
||||
H3["V1Middleware"]
|
||||
H4["RouteMiddleware"]
|
||||
H5["listUsers"]
|
||||
end
|
||||
subgraph sources["来源层级"]
|
||||
S1["全局<br/>Logger, Recovery"]
|
||||
S2["v1 分组<br/>V1Middleware"]
|
||||
S3["路由<br/>RouteMiddleware"]
|
||||
S4["handler<br/>listUsers"]
|
||||
end
|
||||
S1 --> H1
|
||||
S1 --> H2
|
||||
S2 --> H3
|
||||
S3 --> H4
|
||||
S4 --> H5
|
||||
|
||||
c.handlers = [Logger, Recovery, V1Middleware, RouteMiddleware, listUsers]
|
||||
style handlers fill:#F0F0F0
|
||||
style H5 fill:#FFD700
|
||||
```
|
||||
|
||||
注册顺序就是执行顺序:
|
||||
@@ -162,6 +181,8 @@ v1 := r.Group("/api/v1", authMiddleware()) // Logger, Recovery, authMiddlew
|
||||
|
||||
#### CORS 中间件
|
||||
|
||||
允许跨域请求。关键点:必须单独处理 `OPTIONS` 预检请求,否则浏览器会拦截。
|
||||
|
||||
```go
|
||||
func cors() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
@@ -185,6 +206,8 @@ func cors() gin.HandlerFunc {
|
||||
|
||||
#### 认证中间件(JWT)
|
||||
|
||||
从请求头提取 JWT token,校验成功后将用户信息存入 Context,后续 handler 可通过 `c.GetString("userID")` 获取。
|
||||
|
||||
```go
|
||||
func jwtAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
@@ -212,6 +235,8 @@ func jwtAuth() gin.HandlerFunc {
|
||||
|
||||
#### 请求日志(结构化)
|
||||
|
||||
使用 zap/logrus 等结构化日志库,在 `c.Next()` 前后分别记录请求开始和响应状态,便于排查问题。
|
||||
|
||||
```go
|
||||
func structuredLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
@@ -238,6 +263,8 @@ func structuredLogger() gin.HandlerFunc {
|
||||
|
||||
#### 请求 ID 中间件
|
||||
|
||||
为每个请求生成唯一的追踪 ID(RequestID),便于在分布式日志中追踪请求链路。如果客户端已传入则复用。
|
||||
|
||||
```go
|
||||
const requestIDKey = "X-Request-ID"
|
||||
|
||||
@@ -254,7 +281,7 @@ func requestID() gin.HandlerFunc {
|
||||
}
|
||||
```
|
||||
|
||||
> **提问:** 上面的 JWT 中间件中,`c.Set("userID", ...)` 把用户信息存进了 Context。如果认证失败(`c.Abort()`),那这个 `userID` 还会被后面的 handler 读到吗?为什么?
|
||||
> **提问:** JWT 中间件中认证失败时,写入 Context 的 `userID` 会不会被后续 handler 读到?为什么?详见 [[GIN/3-middleware/jwt-auth-qa]]。
|
||||
|
||||
### 5. 中间件中启动 Goroutine 的陷阱
|
||||
|
||||
@@ -319,8 +346,8 @@ r := gin.Default()
|
||||
// 只挂载到 /api 分组
|
||||
api := r.Group("/api", rateLimitMiddleware())
|
||||
{
|
||||
api.GET("/public", publicHandler) // 经过 rateLimit
|
||||
api.GET("/private", privateHandler) // 经过 rateLimit
|
||||
api.GET("/public", publicHandler) // 经过 rateLimit
|
||||
api.GET("/private", privateHandler) // 经过 rateLimit
|
||||
}
|
||||
|
||||
r.GET("/health", healthHandler) // 不经过 rateLimit
|
||||
@@ -358,10 +385,13 @@ func logging() gin.HandlerFunc {
|
||||
| 缓存 | 响应缓存中间件 |
|
||||
| 数据预处理 | 数据注入中间件(如把数据库对象注入 Context) |
|
||||
|
||||
思考题:如果需要在多个分组之间共享中间件(比如 `api/v1` 和 `api/v2` 都需要 CORS),是把 CORS 注册到全局好,还是注册到各自分组好?为什么?
|
||||
思考题:如果需要在多个分组之间共享中间件(比如 `api/v1` 和 `api/v2` 都需要 CORS),是把 CORS 注册到全局好,还是注册到各自分组好?为什么?详见 [[GIN/3-middleware/cors-registration-scope]]。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- `[[GIN/gin-architecture]]` — 中间件链的底层执行机制
|
||||
- `[[GIN/context-lifecycle]]` — `c.Copy()` 的深拷贝原理
|
||||
- `[[GIN/session-auth]]` — 认证/授权中间件实战
|
||||
- [[GIN/3-middleware/jwt-auth-qa]] — JWT 中间件认证失败后 Context 数据安全性
|
||||
- [[GIN/3-middleware/cors-registration-scope]] — 跨分组共享中间件的作用域选择
|
||||
- [[GIN/3-middleware-abort]] — `c.Abort()` 终止机制与常见陷阱
|
||||
- [[GIN/gin-architecture]] — 中间件链的底层执行机制
|
||||
- [[GIN/context-lifecycle]] — `c.Copy()` 的深拷贝原理
|
||||
- [[GIN/session-auth]] — 认证/授权中间件实战
|
||||
@@ -0,0 +1,101 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, 中间件, 作用域]
|
||||
create time: 2026-04-27 12:55
|
||||
---
|
||||
|
||||
# 跨分组共享中间件的作用域选择
|
||||
|
||||
## 概述
|
||||
|
||||
讨论当多个 RouterGroup(如 `/api/v1` 和 `/api/v2`)需要同一中间件时,是注册到全局还是各自分组的取舍。结论:**同策略 → 全局;异策略 → 分组**。
|
||||
|
||||
## 正文
|
||||
|
||||
### 场景还原
|
||||
|
||||
```go
|
||||
v1 := r.Group("/api/v1") // 需要 CORS
|
||||
v2 := r.Group("/api/v2") // 也需要 CORS
|
||||
```
|
||||
|
||||
两种方案:
|
||||
|
||||
**方案 A — 全局注册**(推荐 ✅)
|
||||
|
||||
```go
|
||||
r.Use(cors()) // 一次注册,所有路由自动继承
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
v2 := r.Group("/api/v2")
|
||||
```
|
||||
|
||||
**方案 B — 分组注册**
|
||||
|
||||
```go
|
||||
v1 := r.Group("/api/v1", cors()) // 每个分组都写一遍
|
||||
v2 := r.Group("/api/v2", cors()) // ← 重复代码
|
||||
```
|
||||
|
||||
### 为什么全局更好?
|
||||
|
||||
**1. DRY — 避免重复**
|
||||
|
||||
分组注册需要在每个 Group 构造函数中手动传入中间件,新增版本时容易漏写。
|
||||
|
||||
**2. 不会遗漏 — 更安全**
|
||||
|
||||
```go
|
||||
// 假设后来加了 v3
|
||||
v3 := r.Group("/api/v3") // ← 分组注册下很容易忘记加 cors()
|
||||
// 跨域请求静默失败,排查成本高
|
||||
```
|
||||
|
||||
全局注册一劳永逸,永远不会遗漏。
|
||||
|
||||
**3. OPTIONS 预检拦截天然正确**
|
||||
|
||||
CORS 的核心逻辑是在最外层处理 `OPTIONS` 预检请求:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
OPTIONS["OPTIONS 预检请求"] --> CORS["全局 CORS 中间件<br/>c.AbortWithStatus(204)"]
|
||||
OPTIONS --> SKIP["不进入后续中间件和 handler"]
|
||||
|
||||
style CORS fill:#90EE90
|
||||
style SKIP fill:#FFB6C1
|
||||
```
|
||||
|
||||
放在全局中间件中,预检请求在最早阶段被拦截,不会消耗后续中间件的算力。
|
||||
|
||||
**4. 性能差异可忽略**
|
||||
|
||||
Gin 的 `c.Next()` 只是切片遍历,一个 CORS 中间件多走一趟链的成本微乎其微。为了省这点开销拆分到分组里,得不偿失。
|
||||
|
||||
### 什么时候按分组注册?
|
||||
|
||||
只有一种情况例外:**不同分组需要不同的中间件配置**。
|
||||
|
||||
```go
|
||||
// v1 宽松 — 允许所有来源
|
||||
v1 := r.Group("/api/v1", corsAllowAll())
|
||||
|
||||
// v2 严格 — Origin 白名单校验
|
||||
v2 := r.Group("/api/v2", corsStrict([]string{"https://app.example.com"}))
|
||||
```
|
||||
|
||||
此时策略不同,自然不能复用同一个全局中间件。
|
||||
|
||||
### 决策对照表
|
||||
|
||||
| 条件 | 推荐方案 |
|
||||
|------|---------|
|
||||
| 各分组使用相同中间件配置 | **全局 `r.Use()`** |
|
||||
| 各分组需要不同配置 | 各自分组注册 |
|
||||
| 某些路由明确不需要该中间件 | 跳过全局,按需注册到子分组 |
|
||||
| 中间件本身有副作用(如限流) | 评估后决定,可能需要分组隔离 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[GIN/3-middleware]] — 中间件三级作用域机制
|
||||
- [[GIN/3-middleware-abort]] — `c.Abort()` 终止机制
|
||||
- [[GIN/gin-architecture]] — 中间件链底层执行机制
|
||||
@@ -0,0 +1,89 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, 中间件, net/http]
|
||||
create time: 2026-04-27 13:00
|
||||
---
|
||||
|
||||
# Gin vs net/http 中间件模式对比
|
||||
|
||||
## 概述
|
||||
|
||||
对比 Gin 中间件与 Go 标准库 `net/http` 装饰器模式的本质区别,分析各自在简洁性和灵活性上的优劣。
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. 类型签名差异
|
||||
|
||||
**标准库模式:** `func(http.Handler) http.Handler`
|
||||
- 中间件接收**下一个 handler**,返回**新的 handler**
|
||||
- 本质是**装饰器模式**(Decorator Pattern),层层嵌套
|
||||
- 是**函数式组合**:`middleware3(middleware2(middleware1(handler)))`
|
||||
|
||||
**Gin 模式:** `func(*gin.Context)`
|
||||
- 中间件接收 `*gin.Context`,通过 `c.Next()` **主动推进**到下一个
|
||||
- 本质是**责任链模式**(Chain of Responsibility),串联执行
|
||||
- 是**命令式链式调用**:`m1 → m2 → m3 → handler`
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph stdlib["标准库:装饰器嵌套"]
|
||||
S1["middleware3"] --> S2["middleware2"] --> S3["middleware1"] --> S4["handler"]
|
||||
end
|
||||
subgraph gin["Gin:责任链推进"]
|
||||
G1["m1"] --> G2["c.Next()"] --> G3["m2"] --> G4["c.Next()"] --> G5["handler"]
|
||||
end
|
||||
|
||||
style stdlib fill:#F0F0F0
|
||||
style gin fill:#F0F0F0
|
||||
```
|
||||
|
||||
### 2. 执行控制权
|
||||
|
||||
标准库模式中,中间件**完全控制**是否调用下一个 handler,通过闭包嵌套实现:
|
||||
|
||||
```go
|
||||
// 标准库:闭包嵌套,控制权在闭包内
|
||||
func logging(next http.Handler) http.Handler {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now() // 前置
|
||||
next.ServeHTTP(w, r) // 推进(可选择不调用)
|
||||
// 后置
|
||||
}
|
||||
}
|
||||
// 必须显式嵌套:h := logging(auth(cors(handler)))
|
||||
```
|
||||
|
||||
Gin 模式中,中间件**显式调用** `c.Next()` 推进,写法是线性的:
|
||||
|
||||
```go
|
||||
// Gin:线性写法,c.Next() 就是推进
|
||||
func logging(c *gin.Context) {
|
||||
start := time.Now() // 前置
|
||||
c.Next() // 推进
|
||||
// 后置
|
||||
}
|
||||
// 注册即可:r.Use(logging, auth, cors)
|
||||
```
|
||||
|
||||
### 3. 各自优缺点
|
||||
|
||||
| 维度 | `net/http` 装饰器模式 | Gin 责任链模式 |
|
||||
|------|----------------------|---------------|
|
||||
| **简洁性** | 嵌套深时阅读困难(括号地狱) | 线性注册,一目了然 |
|
||||
| **灵活性** | 高:可以完全跳过 `next`、包装 `ResponseWriter`/`Request` | 中:依赖 `c.Context` 传递状态,`c.Abort()` 中断链 |
|
||||
| **状态传递** | 靠 `context.Context`(类型安全) | 靠 `c.Keys`(`interface{}`,方便但类型不安全) |
|
||||
| **可观测性** | 需要自己包装 `ResponseWriter` 才能读状态码 | `c.Writer.Status()` 直接获取 |
|
||||
| **框架耦合** | 无,纯标准库,可跨框架复用 | 强耦合 Gin 的 `*gin.Context` |
|
||||
| **函数式风格** | 天然支持组合/高阶函数 | 命令式,更像过滤器链 |
|
||||
|
||||
### 4. 结论
|
||||
|
||||
**Gin 的方案更简单,标准库的方案更灵活。**
|
||||
|
||||
- **简单性上** Gin 胜出:线性 `Use()` 注册 + `c.Next()` 推进,不用写嵌套闭包,新人上手快。
|
||||
- **灵活性上** 标准库胜出:你可以用 `http.RoundTripper`、`http.Handler` 包装任意层,甚至写中间件组合子。Gin 被绑定在 `*gin.Context` 上,跨框架复用困难。
|
||||
|
||||
**实际建议**:如果在 Gin 生态内,用 Gin 中间件就够了。如果需要写可复用的中间件库(同时支持 Gin、Echo、标准库),应该写标准库风格的 `func(http.Handler) http.Handler`,然后用适配层桥接到各框架。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[GIN/3-middleware]] — 中间件完整机制
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, 中间件, JWT]
|
||||
create time: 2026-04-27 12:51
|
||||
---
|
||||
|
||||
# JWT 中间件:认证失败后 Context 数据安全性
|
||||
|
||||
## 概述
|
||||
|
||||
回答父文档中提出的思考题:JWT 认证中间件中,如果认证失败并调用了 `c.Abort()`,之前设置的 `userID` 等用户信息是否会被后续 handler 读到?
|
||||
|
||||
## 正文
|
||||
|
||||
### 问题描述
|
||||
|
||||
```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
|
||||
c.Set("userID", claims.UserID)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**问:** 如果认证失败(`c.Abort()`),那 `userID` 还会被后面的 handler 读到吗?为什么?
|
||||
|
||||
### 答案:不会
|
||||
|
||||
#### 原因一:代码层面 — `return` 阻断了执行流
|
||||
|
||||
认证失败分支中,`c.Abort()` 之后紧跟 `return`:
|
||||
|
||||
```go
|
||||
if token == "" {
|
||||
c.JSON(...) // ① 写入 401 响应体
|
||||
c.Abort() // ② 标记终止链
|
||||
return // ③ 函数直接退出
|
||||
}
|
||||
|
||||
c.Set("userID", ...) // ← ④ 永远不会执行到
|
||||
c.Next() // 永远不会执行到
|
||||
```
|
||||
|
||||
`c.Set()` 和 `c.Next()` 在认证通过的分支之后,一旦进入失败分支就会通过 `return` 提前返回,这两行代码根本无法执行。
|
||||
|
||||
#### 原因二:机制层面 — `c.Abort()` 阻断中间件链
|
||||
|
||||
即使忘记写 `return`,Gin 的中间件调度器也会自动阻止后续 handler 执行:
|
||||
|
||||
```go
|
||||
// gin/context.go 核心逻辑
|
||||
func (c *Context) Next() {
|
||||
c.index++
|
||||
for ; c.index < int8(len(c.handlers)); c.index++ {
|
||||
if c.IsAborted() { // ← Abort() 会将 IsAborted 置为 true
|
||||
return
|
||||
}
|
||||
c.handlers[c.index](c)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`c.Abort()` 的作用是让 `IsAborted()` 返回 `true`,导致 `Next()` 中的循环立即终止。
|
||||
|
||||
### 完整的执行链路
|
||||
|
||||
| 步骤 | 动作 | 说明 |
|
||||
|------|------|------|
|
||||
| 1 | `c.JSON(401)` | 写入错误响应体 |
|
||||
| 2 | `c.Abort()` | 设置内部标志位 `isAborted = true` |
|
||||
| 3 | `return` | 中间件函数直接退出 |
|
||||
| 4 | — | `c.Set()` 未执行 → Context 中无 `userID` |
|
||||
| 5 | — | `c.Next()` 未调用 → 后续所有 handler 跳过 |
|
||||
|
||||
### 设计启示
|
||||
|
||||
这个模式体现了一个重要的工程原则:**先失败、快速返回,成功才继续**。
|
||||
|
||||
```
|
||||
验证输入 → 失败? → 快速返回 ────→ 不会污染后续状态
|
||||
↓通过
|
||||
写入上下文 → 交给下游
|
||||
```
|
||||
|
||||
这种 **"Guard Clause"** 风格天然保证了敏感信息(用户身份)只会在验证通过后才被注入到 Context,不会因为代码顺序倒置而意外泄露。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[GIN/3-middleware]] — 中间件完整机制
|
||||
- [[GIN/3-middleware-abort]] — `c.Abort()` 终止机制详解
|
||||
@@ -0,0 +1,226 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, Context, 生命周期]
|
||||
create time: 2026-04-27 00:00
|
||||
---
|
||||
|
||||
# Context 生命周期
|
||||
|
||||
## 概述
|
||||
|
||||
Gin 的 `*gin.Context` 是每次请求的核心载体——它封装了 Request、ResponseWriter、参数、JSON 绑定、中间件链和错误处理。理解 Context 的完整生命周期,是从"会用 Gin"进阶到"写对 Gin"的关键一步。
|
||||
|
||||
思考题:Gin 用 `sync.Pool` 复用 Context 对象,这意味着你**不能**在 handler 返回后继续使用 Context,对吗?为什么?
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. 生命周期总览
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["HTTP 请求到达"] --> B["NewEngine / CreateEngine"]
|
||||
B --> C["sync.Pool 获取 Context"]
|
||||
C --> D["初始化 Context\n(Request, ResponseWriter)"]
|
||||
D --> E["执行 Group 中间件链"]
|
||||
E --> F["匹配路由"]
|
||||
F --> G["执行 Handler 中间件链"]
|
||||
G --> H["执行实际 Handler"]
|
||||
H --> I["中间件逆向退出"]
|
||||
I --> J["Context.Reset() 回收"]
|
||||
J --> K["放回 sync.Pool"]
|
||||
```
|
||||
|
||||
Context 的生命周期严格限定在**单个 HTTP 请求的上下文中**——从请求进入、中间件执行、Handler 处理,到响应写回、Context 回收。
|
||||
|
||||
### 2. Context 的创建与获取
|
||||
|
||||
Gin 通过 `sync.Pool` 池化 Context 对象,避免每次请求都分配内存:
|
||||
|
||||
```go
|
||||
// gin/engine.go 内部
|
||||
func (engine *Engine) serveContext(w http.ResponseWriter, r *http.Request) {
|
||||
c := engine.contextPool.Get().(*Context) // 从池中获取
|
||||
c.reset(w) // 重置所有状态
|
||||
defer engine.contextPool.Put(c) // 请求结束后归还池中
|
||||
|
||||
c.next(r) // 执行中间件链 -> handler
|
||||
w.Write(c.writerMem.Bytes()) // 写回响应
|
||||
}
|
||||
```
|
||||
|
||||
> **关键理解:** `sync.Pool` 复用意味着 Context **不是线程安全的**。不要在 goroutine 中跨请求持有 Context 引用,否则会出现数据混乱。
|
||||
|
||||
### 3. reset 方法:上下文重置
|
||||
|
||||
每次从池中取出 Context 后,`reset` 会清理所有状态,确保请求之间完全隔离:
|
||||
|
||||
```go
|
||||
func (c *Context) reset(w http.ResponseWriter) {
|
||||
c.Writer = w.(*responseWriter)
|
||||
c.writerMem.Reset() // 清空响应缓冲区
|
||||
c.Params = c.Params[:0] // 清空路径参数
|
||||
c.handlers = nil // 清空 handler 链
|
||||
c.index = -1 // 重置执行位置
|
||||
c.errors = c.errors[:0] // 清空错误
|
||||
c.Keys = nil // 清空共享数据
|
||||
c.QueryCache = nil // 清空查询缓存
|
||||
c.FormCache = nil // 清空表单缓存
|
||||
}
|
||||
```
|
||||
|
||||
**提问:** 为什么 `reset` 要把 `index` 重置为 `-1` 而不是 `0`?
|
||||
|
||||
答案:`index` 表示当前在 handler 链中的位置,`-1` 表示还未开始执行。从 `-1` 开始,第一次调用 `c.Next()` 会走到 `index + 1` 即第一个 handler。
|
||||
|
||||
### 4. 中间件链执行——c.Next() 的核心逻辑
|
||||
|
||||
Context 的 handler 链是一个切片数组,`c.Next()` 控制执行流程:
|
||||
|
||||
```go
|
||||
func (c *Context) Next() {
|
||||
c.index++ // 移动到下一个 handler
|
||||
// 依次执行中间件和最终 handler
|
||||
for c.index < len(c.handlers) {
|
||||
c.handlers[c.index](c)
|
||||
c.index++
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**中间件的典型结构:**
|
||||
|
||||
```go
|
||||
func loggingMiddleware(c *gin.Context) {
|
||||
start := time.Now() // 前置逻辑
|
||||
|
||||
// ★ 关键:调用 Next() 执行后续中间件 + handler
|
||||
c.Next()
|
||||
|
||||
// 后置逻辑:在 handler 执行完毕后运行
|
||||
log.Printf("%s %s %v", c.Request.Method, c.Request.URL.Path, time.Since(start))
|
||||
}
|
||||
```
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["中间件 M1 前置"] --> B["M1.Next()"]
|
||||
B --> C["中间件 M2 前置"]
|
||||
C --> D["M2.Next()"]
|
||||
D --> E["Handler 执行"]
|
||||
E --> F["M2 后置逻辑"]
|
||||
F --> G["M1 后置逻辑"]
|
||||
```
|
||||
|
||||
> **关键理解:** 中间件的前置逻辑按注册顺序执行,后置逻辑按**逆序**执行——这和栈的出栈行为一致。
|
||||
|
||||
**提前终止:**
|
||||
|
||||
```go
|
||||
func authMiddleware(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if token == "" {
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "missing token"})
|
||||
return
|
||||
}
|
||||
c.Next() // 校验通过才继续
|
||||
}
|
||||
```
|
||||
|
||||
当调用 `c.Abort()` / `c.AbortWithStatus()` / `c.AbortWithError()` 时,`c.index` 被设为 handlers 长度,`c.Next()` 的循环条件不再满足,链式执行终止。
|
||||
|
||||
### 5. Context 共享数据——c.Keys
|
||||
|
||||
Context 提供 `map[string]interface{}` 的 `Keys` 字段,用于在中间件和 handler 之间共享数据:
|
||||
|
||||
```go
|
||||
func authMiddleware(c *gin.Context) {
|
||||
user, _ := getUserFromToken(c.GetHeader("Authorization"))
|
||||
c.Set("user", user) // 存入 Keys
|
||||
c.Set("request_id", uuid.New())
|
||||
c.Next()
|
||||
}
|
||||
|
||||
func handler(c *gin.Context) {
|
||||
user := c.MustGet("user").(*User) // 安全断言获取
|
||||
// 处理业务逻辑...
|
||||
}
|
||||
```
|
||||
|
||||
**安全性提示:** 如果 key 不存在,`c.Get()` 返回 `(nil, false)` 安全;但 `c.MustGet()` 在 key 不存在时会 **panic**,务必确保数据已被上游中间件设置。
|
||||
|
||||
### 6. Request 与 Response 访问
|
||||
|
||||
Context 完整封装了底层 HTTP 请求与响应对象:
|
||||
|
||||
```go
|
||||
func handler(c *gin.Context) {
|
||||
// 请求信息
|
||||
method := c.Request.Method
|
||||
path := c.Request.URL.Path
|
||||
body := c.Request.Body
|
||||
|
||||
// 便捷方法
|
||||
userID := c.Param("id") // 路径参数 :id
|
||||
page := c.Query("page") // 查询参数 ?page=
|
||||
pageDefault := c.DefaultQuery("page", "1") // 带默认值
|
||||
cookie, _ := c.Cookie("session") // 读取 Cookie
|
||||
lang := c.GetHeader("Accept-Language") // 请求头
|
||||
|
||||
// 响应
|
||||
c.JSON(200, gin.H{"data": "ok"})
|
||||
c.XML(200, gin.H{"status": "ok"})
|
||||
c.Data(200, "text/plain", []byte("plain text"))
|
||||
c.File("./uploads/logo.png") // 静态文件
|
||||
c.Redirect(302, "/next") // 重定向
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 超时与取消
|
||||
|
||||
Context 内置了 `c.Request.Context()` 的取消机制,支持优雅超时:
|
||||
|
||||
```go
|
||||
func longRunningHandler(c *gin.Context) {
|
||||
ctx := c.Request.Context() // 获取底层 context.Context
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
// 客户端断开或 gin.WithHandleHTTPRerrors 超时
|
||||
return
|
||||
case result := <-doWork():
|
||||
c.JSON(200, gin.H{"result": result})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **提问:** Gin 默认有没有超时控制?如果客户端一直不读取响应,服务器资源会不会耗尽?
|
||||
|
||||
Gin 默认**不设置**读取超时。如果需要超时控制,应在 `http.Server` 层配置 `ReadTimeout` / `WriteTimeout`,或在前置中间件中设置自定义超时。
|
||||
|
||||
### 8. Context 方法速查
|
||||
|
||||
| 类别 | 方法 | 作用 |
|
||||
|------|------|------|
|
||||
| 链控制 | `c.Next()` | 执行后续中间件 + handler |
|
||||
| 链控制 | `c.Abort()` | 终止中间件链 |
|
||||
| 参数 | `c.Param("key")` | 路径参数 `:key` |
|
||||
| 参数 | `c.Query("key")` | 查询参数 `?key=` |
|
||||
| 参数 | `c.DefaultQuery("key", "default")` | 带默认值的查询参数 |
|
||||
| 参数 | `c.PostForm("key")` | Form body 参数 |
|
||||
| 绑定 | `c.ShouldBindJSON(&v)` | JSON body 绑定 |
|
||||
| 绑定 | `c.ShouldBindQuery(&v)` | 查询参数绑定 |
|
||||
| 绑定 | `c.ShouldBind(&v)` | 自动检测绑定 |
|
||||
| 共享 | `c.Set(key, val)` | 存储共享数据 |
|
||||
| 共享 | `c.Get(key)` | 获取共享数据 |
|
||||
| 共享 | `c.MustGet(key)` | 安全断言获取(不存在则 panic) |
|
||||
| 响应 | `c.JSON(code, obj)` | JSON 响应 |
|
||||
| 响应 | `c.String(code, format)` | 字符串响应 |
|
||||
| 响应 | `c.Data(code, contentType, bytes)` | 原始数据响应 |
|
||||
| 响应 | `c.File(path)` | 静态文件响应 |
|
||||
| 响应 | `c.Redirect(code, location)` | 重定向 |
|
||||
| 错误 | `c.Error(err)` | 存入 Context 错误列表 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[GIN/1-gin-architecture]] — Engine 初始化与 Context 池化
|
||||
- [[GIN/3-middleware]] — 中间件深入实践
|
||||
- [[GIN/5-binding-validation]] — 模型绑定与校验
|
||||
@@ -24,6 +24,7 @@ create time: 2026-04-27 00:00
|
||||
| 3 | `middleware.md` | 使用中间件 + 自定义中间件 + 中间件中的 Goroutine + 安全头 | 中间件链执行顺序(全局 → 分组 → 路由);`gin.HandlerFunc` 的本质;常用中间件实现模板(CORS、限流、鉴权、RequestID、安全头);中间件中启动 Goroutine 的陷阱与 `c.Copy()` 用法 |
|
||||
| 4 | `context-lifecycle.md` | 上下文与取消 | `*gin.Context` 底层设计:keys/values map、Request/RW 包装;`c.Set/Get/GetString`;`c.Copy()` 的深拷贝边界;请求级 `context` 传播与取消 |
|
||||
| 5 | `binding-validation.md` | 模型绑定和验证 + 自定义验证器 + 绑定查询字符串 + 绑定自定义反序列化器 + 绑定请求头 + 绑定 URI + 绑定 HTML 复选框 | `ShouldBind` 全家桶(JSON、form、query、header、uri);`binding` 标签内置规则速查;自定义 Validator(`.RegisterValidation`);自定义反序列化器(`bind.DeferredBinder`);数组集合格式(`UserIds[]`) |
|
||||
| 6 | `handler-relationship.md` | — | Gin 与 `http.Handler` 的关系:`*gin.Engine` 实现 `http.Handler` 接口的含义;等价于 `http.ListenAndServe` 启动;标准库与 Gin 的兼容边界 |
|
||||
|
||||
### 二、进阶功能
|
||||
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, Context, 底层]
|
||||
create time: 2026-04-27 00:00
|
||||
---
|
||||
|
||||
# Context 生命周期与底层设计
|
||||
|
||||
## 概述
|
||||
|
||||
`*gin.Context` 是 Gin 框架中最核心的类型——它封装了整个请求的生命周期:参数解析、请求/响应读写、错误收集、数据传递、取消传播。理解它的底层设计,能帮你避开绝大多数陷阱,尤其是 goroutine 安全和内存泄漏。
|
||||
|
||||
思考题:`gin.Context` 和 Go 标准库的 `context.Context` 是同一个东西吗?如果不是,它们怎么协作?
|
||||
|
||||
## 正文
|
||||
|
||||
### 1. gin.Context 的结构
|
||||
|
||||
```go
|
||||
type Context struct {
|
||||
writermem *responseWriter // 缓冲的响应写入器
|
||||
Request *http.Request // 原始 HTTP 请求(只读)
|
||||
fullPath string // 完整路由路径,如 /api/v1/users/:id
|
||||
handlers HandlersChain // 待执行的 handler 链
|
||||
index int8 // 当前执行到的 handler 索引
|
||||
engine *Engine // 回指全局引擎
|
||||
params *Params // 路径参数
|
||||
keys map[string]any // 请求级 key-value 存储
|
||||
errors ErrorList // 错误链(c.Error 收集的)
|
||||
accepted []string // Accept 头解析
|
||||
flush func() // 刷写回调
|
||||
}
|
||||
```
|
||||
|
||||
**设计要点:**
|
||||
|
||||
| 字段 | 类型 | 作用 | 备注 |
|
||||
|------|------|------|------|
|
||||
| `keys` | `map[string]any` | handler 间数据传递 | 请求级隔离,每次请求独立 |
|
||||
| `errors` | `ErrorList` | 错误链收集 | 多个 `c.Error()` 可以累积 |
|
||||
| `writermem` | `*responseWriter` | 缓冲写入 | 默认 4KB 缓冲区,减少 syscall |
|
||||
| `params` | `*Params` | 路径参数缓存 | 避免重复解析 |
|
||||
| `handlers` | `HandlersChain` | handler 链 | 全局复用切片,`c.Next()` 推进 |
|
||||
|
||||
### 2. Context 的创建与回收
|
||||
|
||||
Gin 使用 `sync.Pool` 复用 Context 对象,避免频繁 GC:
|
||||
|
||||
```
|
||||
请求进来: pool.Get() → 初始化 → 路由匹配 → 执行 handler → pool.Put()
|
||||
```
|
||||
|
||||
```go
|
||||
// gin/engine.go
|
||||
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
// 从 pool 取出一个 Context
|
||||
c := engine.getContext() // sync.Pool.Get()
|
||||
defer engine.freeContext(c) // pool.Put() — 请求结束归还
|
||||
|
||||
c.writermem.reset(w) // 重置响应写入器
|
||||
c.Request = req // 挂上请求
|
||||
c.index = -1 // 索引归零
|
||||
c.errors = c.errors[:0] // 清空错误链
|
||||
c.keys = nil // 清空键值存储
|
||||
|
||||
// 路由匹配 → 分配 handler 链
|
||||
handlers, params, _ := engine.tree.match(req.URL.Path, req.Method)
|
||||
c.handlers = handlers
|
||||
c.params = params
|
||||
|
||||
// 开始执行
|
||||
c.Next() // 推进 handler 链
|
||||
}
|
||||
```
|
||||
|
||||
**关键结论:** Context 的生命周期 = 单次请求。请求结束后 Context 立即被回收复用,**绝不能持有 Context 引用**。
|
||||
|
||||
> **提问:** 如果一个 handler 中 `go func() { _ = c.Request }()` 启动了一个 goroutine,请求结束后 Context 被 pool 回收,这个 goroutine 会 panic 吗?为什么 `c.Copy()` 能解决这个问题?
|
||||
|
||||
### 3. c.Set / c.Get — 请求间数据传递
|
||||
|
||||
`c.Set` 和 `c.Get` 是在 handler 之间传递数据的标准方式:
|
||||
|
||||
```go
|
||||
// 中间件中设置
|
||||
authMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
claims, _ := parseJWT(c.GetHeader("Authorization"))
|
||||
c.Set("userID", claims.UserID) // 存入 Context
|
||||
c.Set("role", claims.Role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// handler 中读取
|
||||
func profileHandler(c *gin.Context) {
|
||||
userID := c.GetString("userID") // "123"
|
||||
role := c.GetString("role") // "admin"
|
||||
|
||||
// 如果 key 不存在,GetString 返回零值
|
||||
if userID == "" {
|
||||
c.JSON(401, gin.H{"message": "未认证"})
|
||||
return
|
||||
}
|
||||
c.JSON(200, gin.H{"user_id": userID, "role": role})
|
||||
}
|
||||
```
|
||||
|
||||
**类型安全的读取方式:**
|
||||
|
||||
```go
|
||||
// GetString — key 不存在时返回 ""
|
||||
v := c.GetString("key")
|
||||
|
||||
// GetStringOk — 返回 (value, ok)
|
||||
v, ok := c.GetStringOk("key")
|
||||
|
||||
// Get — 返回任意类型,需要类型断言
|
||||
v, ok := c.Get("key")
|
||||
name, ok := v.(string)
|
||||
|
||||
// 强类型封装
|
||||
func getUserID(c *gin.Context) string {
|
||||
id, _ := c.GetStringOk("userID")
|
||||
return id
|
||||
}
|
||||
```
|
||||
|
||||
> **提问:** `c.Set` 的 `keys` map 在每次请求结束后会被清空吗?如果不清空,pool 复用下一个请求时会读到上一个请求的数据吗?
|
||||
|
||||
### 4. c.Next() 与 c.Abort() 的控制流
|
||||
|
||||
这是理解中间件执行顺序的核心:
|
||||
|
||||
```
|
||||
c.Next() → 继续执行下一个 handler
|
||||
c.Abort() → 停止执行后续 handler
|
||||
c.AbortWithStatus(code) → 停止执行,并设置 HTTP 状态码
|
||||
c.AbortWithStatusJSON(code, obj) → 停止执行,返回 JSON 响应
|
||||
```
|
||||
|
||||
```go
|
||||
func authMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if !validateToken(token) {
|
||||
// 终止中间件链
|
||||
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
|
||||
return // 注意:return 仍然执行,但 Abort 已经阻止了 Next()
|
||||
}
|
||||
c.Next() // 继续
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**三种控制流模式:**
|
||||
|
||||
```go
|
||||
// 模式1:正常传递 — c.Next() 前后都有代码
|
||||
func middleware1(c *gin.Context) {
|
||||
fmt.Println("before")
|
||||
c.Next() // 传递
|
||||
fmt.Println("after") // handler 返回后继续
|
||||
}
|
||||
|
||||
// 模式2:提前终止 — c.Abort()
|
||||
func middleware2(c *gin.Context) {
|
||||
fmt.Println("before")
|
||||
c.Abort()
|
||||
fmt.Println("never reached") // 这行不会执行(因为 return 了)
|
||||
}
|
||||
|
||||
// 模式3:条件传递 — 不满足条件直接 return,不调用 Next()
|
||||
func middleware3(c *gin.Context) {
|
||||
if skipCondition {
|
||||
return // 不传递到 handler,直接结束
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
```
|
||||
|
||||
### 5. c.Copy() 深拷贝原理
|
||||
|
||||
`c.Copy()` 创建一个独立的 `*gin.Context` 副本,用于异步 goroutine:
|
||||
|
||||
```go
|
||||
func handler(c *gin.Context) {
|
||||
c.Set("userID", "123")
|
||||
|
||||
copy := c.Copy() // 创建副本
|
||||
go func() {
|
||||
// 安全的异步操作 — 只读
|
||||
log.Printf("用户 %s 的异步任务完成", copy.GetString("userID"))
|
||||
// 注意:不能调用 copy.JSON() 写响应!
|
||||
}()
|
||||
}
|
||||
```
|
||||
|
||||
**`c.Copy()` 的深拷贝范围:**
|
||||
|
||||
| 复制到副本 | 说明 |
|
||||
|-----------|------|
|
||||
| `c.Request` | 原始请求的浅拷贝(`*http.Request`) |
|
||||
| `c.Keys` | 完整深拷贝 `map[string]any` |
|
||||
| `c.Params` | `*Params` 指针引用(相同数据) |
|
||||
| `c.Writer` | **不会被复制**,副本的 Writer 无效 |
|
||||
| `c.Errors` | 不会被复制 |
|
||||
| `c.handlers` / `c.index` | 不会被复制 |
|
||||
|
||||
**重要限制:** 副本不能写响应,因为 `Writer` 没有被复制。这是有意设计的——异步 goroutine 不应该修改当前请求的响应。
|
||||
|
||||
思考题:`c.Keys` 是深拷贝,意味着对副本中 `keys` map 的修改不会影响原始 Context。但如果 map 中存的是一个指针(`c.Set("db", &DB{})`),那拷贝的是指针还是指针指向的对象?
|
||||
|
||||
### 6. gin.Context 与 context.Context 的关系
|
||||
|
||||
很多人混淆这两个 Context,它们**完全不同**:
|
||||
|
||||
```
|
||||
gin.Context — Gin 的请求封装,包含请求/响应/参数/错误等
|
||||
context.Context — Go 标准的上下文传递,主要用于取消传播和超时控制
|
||||
```
|
||||
|
||||
**它们的交集:** `c.Request.Context()`
|
||||
|
||||
```go
|
||||
func handler(c *gin.Context) {
|
||||
// gin.Context → Go 标准 context
|
||||
ctx := c.Request.Context() // 获取标准 context
|
||||
ctx.Done() // 监听客户端断开
|
||||
ctx.Err() // 获取取消原因
|
||||
|
||||
// 可以创建带超时的 context
|
||||
newCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 在 context 取消时立即停止数据库查询
|
||||
result, err := db.Query(newCtx, "SELECT * FROM users")
|
||||
if newCtx.Err() != nil {
|
||||
c.JSON(504, gin.H{"error": "request timeout"})
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关系图:**
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["gin.Context"] -->|"包含"| B["*http.Request"]
|
||||
B -->|"包含"| C["context.Context"]
|
||||
A -->|"提供"| D["c.Set / c.Get — 请求数据"]
|
||||
A -->|"提供"| E["c.JSON / c.String — 响应"]
|
||||
A -->|"提供"| F["c.Param — 路径参数"]
|
||||
C -->|"提供"| G["Done / Err — 取消传播"]
|
||||
C -->|"提供"| H["WithTimeout / WithCancel — 超时控制"]
|
||||
|
||||
style A fill:#e3f2fd,stroke:#1565c0
|
||||
style C fill:#f3e5f5,stroke:#7b1fa2
|
||||
```
|
||||
|
||||
> **关键理解:** `gin.Context` 包含 `context.Context`(通过 Request),但不**是** `context.Context`。`gin.Context` 不能直接传给需要 `context.Context` 的函数。
|
||||
|
||||
**常见错误:**
|
||||
|
||||
```go
|
||||
// 错误:把 gin.Context 当 context.Context 用
|
||||
func doSomething(ctx context.Context) { ... }
|
||||
|
||||
func handler(c *gin.Context) {
|
||||
doSomething(c) // ❌ 类型不匹配!
|
||||
}
|
||||
|
||||
// 正确:提取 c.Request.Context()
|
||||
func handler(c *gin.Context) {
|
||||
doSomething(c.Request.Context()) // ✅
|
||||
}
|
||||
```
|
||||
|
||||
### 7. 请求取消与超时控制
|
||||
|
||||
生产环境中必须处理客户端断开连接的场景:
|
||||
|
||||
```go
|
||||
func longRunningHandler(c *gin.Context) {
|
||||
// 获取标准 context(包含客户端断开信号)
|
||||
ctx := c.Request.Context()
|
||||
|
||||
// 同时设置请求级超时
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// 把 context 传递给数据库/HTTP 调用
|
||||
user, err := db.GetUser(ctx, id)
|
||||
if err != nil {
|
||||
if ctx.Err() == context.Canceled {
|
||||
c.JSON(499, gin.H{"error": "client disconnected"})
|
||||
} else if ctx.Err() == context.DeadlineExceeded {
|
||||
c.JSON(504, gin.H{"error": "timeout"})
|
||||
} else {
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, gin.H{"user": user})
|
||||
}
|
||||
```
|
||||
|
||||
**超时控制的层级:**
|
||||
|
||||
```
|
||||
1. Gin 配置 — r.MaxRequestDuration(已废弃,用 context.WithTimeout)
|
||||
2. context.WithTimeout — handler 级超时
|
||||
3. context.WithCancel — handler 级取消
|
||||
4. context.WithValue — 传递请求级值
|
||||
5. gin.Recovery — 极端情况下的 panic 恢复
|
||||
```
|
||||
|
||||
### 8. Context 使用的常见陷阱
|
||||
|
||||
| 陷阱 | 说明 | 正确做法 |
|
||||
|------|------|----------|
|
||||
| 持有 Context 引用 | 请求结束后 Context 被 pool 回收 | 用 `c.Copy()` 或只传值 |
|
||||
| 在 goroutine 中写响应 | `c.JSON()` 在 goroutine 中会导致并发问题 | 异步任务不写响应 |
|
||||
| 用 gin.Context 替代 context.Context | 类型不匹配,API 不通用 | 用 `c.Request.Context()` |
|
||||
| 在 handler 中修改 c.Request.Body | 只能读一次,重复读取为空 | 提前读取并缓存 |
|
||||
| keys map 存大对象 | 每个请求的 map 都是新分配 | 只存引用类型(指针)或基础类型 |
|
||||
|
||||
思考题:如果在一个 handler 中调用了 `c.Set("data", largeStruct)`,而这个 struct 很大(比如 1MB),这会造成什么问题?Pool 复用的时机对内存管理有什么影响?
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- `[[GIN/gin-architecture]]` — Context 的创建/回收流程
|
||||
- `[[GIN/middleware]]` — 中间件中用 `c.Copy()` 的陷阱
|
||||
- `[[GIN/request-context]]` — 请求级 context 传播实战
|
||||
Reference in New Issue
Block a user