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 了解结构、规范和惯例。 +如果用户提及创建子文档,意思为创建同名子文件夹,在子文件夹下创建文档,父文档中适当位置插入新文档链接。 在初次完善后,读取文件并二次检查是否符合文档的每一条内容规范—— - **教学者模式** 提问设计 - **代码示例** 合理注释