Init
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
---
|
||||
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` 预检请求——预检触发条件、两轮对话机制、Header 语义详见 [[BACKEND/GIN/3-middleware/cors-registration-scope/cors-preflight|cors-preflight]]。
|
||||
|
||||
放在全局中间件中,预检请求在最早阶段被拦截,不会消耗后续中间件的算力。
|
||||
|
||||
**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/cors-preflight]] — OPTIONS 预检请求完整机制(触发条件、两轮对话、Max-Age 缓存)
|
||||
- [[GIN/3-middleware]] — 中间件三级作用域机制
|
||||
- [[middleware-abort]] — `c.Abort()` 终止机制
|
||||
- [[GIN/gin-architecture]] — 中间件链底层执行机制
|
||||
@@ -0,0 +1,184 @@
|
||||
---
|
||||
tags: [后端, Go, Gin, CORS, 跨域, 预检]
|
||||
create time: 2026-04-27 13:00
|
||||
---
|
||||
|
||||
# CORS OPTIONS 预检请求详解
|
||||
|
||||
## 概述
|
||||
|
||||
当浏览器发起**跨域非简单请求**时,会在实际请求之前先发送一个 `OPTIONS` 预检请求,询问服务端是否允许该操作。本文完整解析预检触发条件、两轮对话流程及底层机制。
|
||||
|
||||
## 正文
|
||||
|
||||
### 什么时候触发预检?
|
||||
|
||||
并非所有跨域请求都经过预检。以下四种情况**之一满足即触发**:
|
||||
|
||||
| 触发条件 | 说明 |
|
||||
|---------|------|
|
||||
| 方法不是简单方法 | 使用了 `PUT`、`DELETE`、`PATCH`、`HEAD` 等 |
|
||||
| 请求头包含自定义 Header | 如 `X-Token`、`Authorization`(`Accept`/`Content-Type`/`Language` 除外) |
|
||||
| `Content-Type` 是非简单类型 | 如 `application/json`;`text/plain`、`multipart/form-data`、`application/x-www-form-urlencoded` 是简单类型 |
|
||||
| 跨域且携带凭证 | `withCredentials: true` 或 `credentials: "include"` |
|
||||
|
||||
> **思考**:为什么同源请求不需要预检?因为同源策略本身就已经限制了跨站操作的安全性,不需要额外协商。
|
||||
|
||||
### 预检的两轮对话
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant B as 浏览器
|
||||
participant S as 服务端
|
||||
|
||||
B->>S: OPTIONS /api/resource
|
||||
Note over B,S: Origin<br/>Access-Control-Request-Method<br/>Access-Control-Request-Headers
|
||||
S-->>B: 204 No Content
|
||||
Note over B,S: Access-Control-Allow-Origin<br/>Access-Control-Allow-Methods<br/>Access-Control-Allow-Headers<br/>Access-Control-Max-Age
|
||||
B->>S: PUT /api/resource
|
||||
Note over B,S: Origin<br/>X-Token: xxx
|
||||
S-->>B: 200 OK + CORS headers
|
||||
|
||||
rect rgba(200, 255, 200, 0.3)
|
||||
Note over B,B: ① 预检请求
|
||||
end
|
||||
rect rgba(255, 255, 200, 0.3)
|
||||
Note over S,S: ② 预检响应
|
||||
end
|
||||
rect rgba(200, 200, 255, 0.3)
|
||||
Note over B,B: ③ 实际请求
|
||||
end
|
||||
rect rgba(255, 200, 200, 0.3)
|
||||
Note over S,S: ④ 实际响应
|
||||
end
|
||||
```
|
||||
|
||||
#### ① 预检请求 — 浏览器发问
|
||||
|
||||
```http
|
||||
OPTIONS /api/resource HTTP/1.1
|
||||
Host: api.example.com
|
||||
Origin: https://app.example.com
|
||||
Access-Control-Request-Method: PUT
|
||||
Access-Control-Request-Headers: X-Token, Content-Type
|
||||
```
|
||||
|
||||
两个关键自定义 Header 由浏览器**自动添加**,开发者无法操控:
|
||||
- `Access-Control-Request-Method` — 打算用的 HTTP 方法
|
||||
- `Access-Control-Request-Headers` — 打算带的自定义 Header 列表
|
||||
|
||||
#### ② 预检响应 — 服务端答复
|
||||
|
||||
```http
|
||||
HTTP/1.1 204 No Content
|
||||
Access-Control-Allow-Origin: https://app.example.com
|
||||
Access-Control-Allow-Methods: PUT, GET, POST, DELETE
|
||||
Access-Control-Allow-Headers: X-Token, Content-Type
|
||||
Access-Control-Max-Age: 86400
|
||||
```
|
||||
|
||||
注意用 `204 No Content`——预检**没有响应体**,因为还没确认要处理实际请求。
|
||||
|
||||
#### ③ 实际请求 — 预检通过后才发
|
||||
|
||||
```http
|
||||
PUT /api/resource HTTP/1.1
|
||||
Host: api.example.com
|
||||
Origin: https://app.example.com
|
||||
X-Token: eyJhbG...
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
如果预检失败(收到非 2xx 或缺少必要 header),浏览器直接**静默拒绝**,不会发送实际请求。
|
||||
|
||||
#### ④ 实际响应 — 浏览器二次校验
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Access-Control-Allow-Origin: https://app.example.com
|
||||
Access-Control-Allow-Credentials: true
|
||||
Content-Type: application/json
|
||||
|
||||
```
|
||||
|
||||
浏览器的实际请求**不经过预检**,但浏览器仍会检查响应中的 CORS header 是否匹配当前页面来源。
|
||||
|
||||
### 关键 Header 速查
|
||||
|
||||
| 请求/Header | 方向 | 作用 |
|
||||
|-------------|------|------|
|
||||
| `Origin` | → 和 ↩ | 当前页面的源(协议+域名+端口),**不可自定义** |
|
||||
| `Access-Control-Allow-Origin` | ↩ | 允许的源;与 `credentials: true` 搭配时不能为 `*` |
|
||||
| `Access-Control-Allow-Methods` | ↩ | 预检通过的 HTTP 方法列表(逗号分隔) |
|
||||
| `Access-Control-Allow-Headers` | ↩ | 预检通过的自定义 Header 列表 |
|
||||
| `Access-Control-Max-Age` | ↩ | 预检结果缓存时长(秒),期间不再发 OPTIONS |
|
||||
| `Access-Control-Allow-Credentials` | ↩ | 是否允许带 Cookie/认证信息 |
|
||||
| `Access-Control-Expose-Headers` | ↩ | 允许 JS `getResponseHeader()` 读取的响应头白名单 |
|
||||
|
||||
> **常见坑**:`Allow-Credentials: true` 时 `Allow-Origin` 必须写死具体域名,不能用 `*`。这是浏览器强制校验的安全策略。
|
||||
|
||||
### Max-Age 缓存机制
|
||||
|
||||
浏览器会对成功的预检结果做缓存:
|
||||
|
||||
- `Max-Age: 86400` = 24 小时内再次访问同一路径,浏览器**跳过预检**直接发实际请求
|
||||
- 不同路径、不同方法、不同 Origin 视为不同的预检条目,各自独立缓存
|
||||
- 如果服务端返回 `Max-Age: 0`,浏览器每次都会预检
|
||||
|
||||
这也是为什么优化跨域性能时,把 `Max-Age` 设置大一些比取消中间件顺序更有意义。
|
||||
|
||||
### Gin 中的实现要点
|
||||
|
||||
对应的中间件逻辑非常简单——**只要区分是否是 OPTIONS 请求**:
|
||||
|
||||
```go
|
||||
func CORSMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
// 预检请求:只设 header + 拦截
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Token")
|
||||
c.AbortWithStatus(http.StatusNoContent) // 204, 无 Body
|
||||
return
|
||||
}
|
||||
// 实际请求:正常走链,但响应头里也要注入 CORS(供浏览器校验)
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
核心就两件事:
|
||||
- **OPTIONS → 只设 header,`c.Abort()` 终止链**,不进任何 handler
|
||||
- **其他方法 → 正常请求,但 `c.Header()` 注入 header 到最终响应**(浏览器的实际请求需要这些 header 来确认服务端确实允许)
|
||||
|
||||
这就是为什么它适合注册为全局中间件——不在路由匹配前拦截预检的话,Gin 会因为找不到 `OPTIONS` 路由而返回 405/404,浏览器认为预检失败就直接阻塞了后续所有请求。
|
||||
|
||||
### 前后端对照
|
||||
|
||||
同一接口的完整交互视角(前端发起):
|
||||
|
||||
```ts
|
||||
// 前端代码
|
||||
fetch("https://api.example.com/resource", {
|
||||
method: "PUT",
|
||||
credentials: "include", // ← 触发携带凭证模式
|
||||
headers: {
|
||||
"Content-Type": "application/json", // ← 触发预检(非简单 Content-Type)
|
||||
"X-Token": token, // ← 触发预检(自定义 Header)
|
||||
},
|
||||
body: JSON.stringify({ name: "updated" }),
|
||||
})
|
||||
```
|
||||
|
||||
上述代码在实际发 `PUT` 请求前,浏览器必定先发一轮 `OPTIONS`。如果把其中任意一个触发条件去掉(比如改用 `GET` + `text/plain`),就可以省去预检往返。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[cors-registration-scope]] — 全局 vs 分组注册 CORS 中间件的取舍
|
||||
- [[GIN/3-middleware]] — 中间件三级作用域机制
|
||||
- [[middleware-abort]] — `c.Abort()` 终止机制
|
||||
- [[GIN/3-middleware]] — 中间件三级作用域机制
|
||||
- [[middleware-abort]] — `c.Abort()` 终止机制
|
||||
@@ -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]] — 中间件完整机制
|
||||
- [[middleware-abort]] — `c.Abort()` 终止机制详解
|
||||
@@ -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 的深拷贝原理
|
||||
Reference in New Issue
Block a user