diff --git a/BACKEND/GIN/gin-architecture.md b/BACKEND/GIN/1-gin-architecture.md
similarity index 98%
rename from BACKEND/GIN/gin-architecture.md
rename to BACKEND/GIN/1-gin-architecture.md
index 8712947..70cc765 100644
--- a/BACKEND/GIN/gin-architecture.md
+++ b/BACKEND/GIN/1-gin-architecture.md
@@ -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 基础用法入门
diff --git a/BACKEND/GIN/1-gin-architecture/context-pool.md b/BACKEND/GIN/1-gin-architecture/context-pool.md
new file mode 100644
index 0000000..0226e1c
--- /dev/null
+++ b/BACKEND/GIN/1-gin-architecture/context-pool.md
@@ -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 的关系
diff --git a/BACKEND/GIN/1-gin-architecture/engine-handler.md b/BACKEND/GIN/1-gin-architecture/engine-handler.md
new file mode 100644
index 0000000..0b82d3d
--- /dev/null
+++ b/BACKEND/GIN/1-gin-architecture/engine-handler.md
@@ -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 的底层设计
diff --git a/BACKEND/GIN/routing.md b/BACKEND/GIN/2-routing.md
similarity index 99%
rename from BACKEND/GIN/routing.md
rename to BACKEND/GIN/2-routing.md
index a381280..59fddb0 100644
--- a/BACKEND/GIN/routing.md
+++ b/BACKEND/GIN/2-routing.md
@@ -11,6 +11,8 @@ Gin 的路由系统是整个框架的性能核心——它用 **Radix Tree(基
思考题:Gin 的路由匹配比 `http.ServeMux` 快多少?为什么?(提示:思考标准库 ServeMux 匹配一个 URL 需要遍历什么)
+> 详细解答见:[[2-routing/2-routing-complexity-comparison]]
+
## 正文
### 1. 路由注册
diff --git a/BACKEND/GIN/2-routing/2-routing-complexity-comparison.md b/BACKEND/GIN/2-routing/2-routing-complexity-comparison.md
new file mode 100644
index 0000000..77179a3
--- /dev/null
+++ b/BACKEND/GIN/2-routing/2-routing-complexity-comparison.md
@@ -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 路由匹配与分组机制
diff --git a/BACKEND/GIN/3-middleware-abort.md b/BACKEND/GIN/3-middleware-abort.md
new file mode 100644
index 0000000..3064e3e
--- /dev/null
+++ b/BACKEND/GIN/3-middleware-abort.md
@@ -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 的深拷贝原理
diff --git a/BACKEND/GIN/middleware.md b/BACKEND/GIN/3-middleware.md
similarity index 66%
rename from BACKEND/GIN/middleware.md
rename to BACKEND/GIN/3-middleware.md
index 91489ee..745d201 100644
--- a/BACKEND/GIN/middleware.md
+++ b/BACKEND/GIN/3-middleware.md
@@ -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 级
全局中间件"] --> v1["RouterGroup /api/v1
分组中间件"]
+ Engine --> v2["RouterGroup /api/v2
分组中间件"]
+ v1 --> u1["GET /users
路由中间件"]
+ v1 --> p1["GET /posts
路由中间件"]
+ v2 --> u2["GET /users
路由中间件"]
```
**执行顺序:** 全局 → 分组 → 路由 → 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["全局
Logger, Recovery"]
+ S2["v1 分组
V1Middleware"]
+ S3["路由
RouteMiddleware"]
+ S4["handler
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]] — 认证/授权中间件实战
diff --git a/BACKEND/GIN/3-middleware/cors-registration-scope.md b/BACKEND/GIN/3-middleware/cors-registration-scope.md
new file mode 100644
index 0000000..eeae445
--- /dev/null
+++ b/BACKEND/GIN/3-middleware/cors-registration-scope.md
@@ -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 中间件
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]] — 中间件链底层执行机制
diff --git a/BACKEND/GIN/3-middleware/gin-vs-std.md b/BACKEND/GIN/3-middleware/gin-vs-std.md
new file mode 100644
index 0000000..f51d4af
--- /dev/null
+++ b/BACKEND/GIN/3-middleware/gin-vs-std.md
@@ -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]] — 中间件完整机制
diff --git a/BACKEND/GIN/3-middleware/jwt-auth-qa.md b/BACKEND/GIN/3-middleware/jwt-auth-qa.md
new file mode 100644
index 0000000..2237d28
--- /dev/null
+++ b/BACKEND/GIN/3-middleware/jwt-auth-qa.md
@@ -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()` 终止机制详解
diff --git a/BACKEND/GIN/4-context-lifecycle.md b/BACKEND/GIN/4-context-lifecycle.md
new file mode 100644
index 0000000..c262ced
--- /dev/null
+++ b/BACKEND/GIN/4-context-lifecycle.md
@@ -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]] — 模型绑定与校验
diff --git a/BACKEND/GIN/binding-validation.md b/BACKEND/GIN/5-binding-validation.md
similarity index 100%
rename from BACKEND/GIN/binding-validation.md
rename to BACKEND/GIN/5-binding-validation.md
diff --git a/BACKEND/GIN/README.md b/BACKEND/GIN/README.md
index c5ad035..6690d97 100644
--- a/BACKEND/GIN/README.md
+++ b/BACKEND/GIN/README.md
@@ -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 的兼容边界 |
### 二、进阶功能
diff --git a/BACKEND/GIN/context-lifecycle.md b/BACKEND/GIN/context-lifecycle.md
deleted file mode 100644
index 9aa1719..0000000
--- a/BACKEND/GIN/context-lifecycle.md
+++ /dev/null
@@ -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 传播实战
diff --git a/CLAUDE.md b/CLAUDE.md
index a380550..bbed836 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,11 +1,13 @@
# Claude Code 配置 - Obsidian 知识库
此知识库级别的配置为 Claude 操作提供额外的指导说明。
+在行动前,先列计划(工具调用)。
## 新增和完善文件
在初始化和完成文件时,必须**完整**读取参照 `./config/agent/DOCUMENT_OPERATION.md
了解结构、规范和惯例。
+如果用户提及创建子文档,意思为创建同名子文件夹,在子文件夹下创建文档,父文档中适当位置插入新文档链接。
在初次完善后,读取文件并二次检查是否符合文档的每一条内容规范——
- **教学者模式** 提问设计
- **代码示例** 合理注释