229 lines
7.8 KiB
Markdown
229 lines
7.8 KiB
Markdown
---
|
||
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,对吗?为什么?
|
||
详见 → [[4-context-lifecycle/context-pool-safety]]
|
||
|
||
## 正文
|
||
|
||
### 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]] — 模型绑定与校验
|
||
- [[4-context-lifecycle/context-pool-safety]] — sync.Pool 复用安全:handler 返回后为什么不能持有 Context
|