vault backup: 2026-04-27 22:55:05

This commit is contained in:
2026-04-27 22:55:05 +08:00
parent 301abb5106
commit 2df3748fc9
15 changed files with 1155 additions and 386 deletions
@@ -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]] — 中间件链底层执行机制
+89
View File
@@ -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]] — 中间件完整机制
+105
View File
@@ -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()` 终止机制详解