110 lines
3.7 KiB
Markdown
110 lines
3.7 KiB
Markdown
|
|
---
|
|||
|
|
tags:
|
|||
|
|
- 后端
|
|||
|
|
- Go
|
|||
|
|
- Gin
|
|||
|
|
- Context
|
|||
|
|
- 生命周期
|
|||
|
|
create time: 2026-04-27 00:00
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# sync.Pool 与 Context 复用安全
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
Gin 通过 `sync.Pool` 池化 `*gin.Context` 来减少内存分配开销,但这带来了一个关键的安全约束:**handler 返回后不能再持有 Context 引用**。本文深入解析这一行为背后的三个原因及正确做法。
|
|||
|
|
|
|||
|
|
## 正文
|
|||
|
|
|
|||
|
|
### 思考题回顾
|
|||
|
|
|
|||
|
|
> **问题:** Gin 用 `sync.Pool` 复用 Context 对象,这意味着你**不能**在 handler 返回后继续使用 Context,对吗?为什么?
|
|||
|
|
|
|||
|
|
### 结论
|
|||
|
|
|
|||
|
|
handler 返回后绝对不能再持有或使用 Context 引用。
|
|||
|
|
|
|||
|
|
### 原因一:Context 会被立即归还到池中
|
|||
|
|
|
|||
|
|
`serveContext` 中通过 `defer` 将 Context 归还给池——handler 一 `return`,defer 立即执行:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (engine *Engine) serveContext(w http.ResponseWriter, r *http.Request) {
|
|||
|
|
c := engine.contextPool.Get().(*Context)
|
|||
|
|
c.reset(w)
|
|||
|
|
defer engine.contextPool.Put(c) // ← handler return 后立即归还
|
|||
|
|
|
|||
|
|
c.next(r) // 执行中间件链 → handler
|
|||
|
|
w.Write(c.writerMem.Bytes())
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 原因二:下一个请求可能立刻拿到同一个对象
|
|||
|
|
|
|||
|
|
`sync.Pool` 是"取出→重置→复用→归还"的循环:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant Pool as sync.Pool
|
|||
|
|
participant A as 请求 A
|
|||
|
|
participant B as 请求 B
|
|||
|
|
|
|||
|
|
A->>Pool: Get() → reset() → 处理 → Put()
|
|||
|
|
B->>Pool: Get() → reset() → 处理 → Put()
|
|||
|
|
Note over A,B: 同一内存地址被复用
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
当旧 Context 被 Put 回池后,另一个请求可能从池中 Get() 出**完全相同的对象**(同一内存地址)。此时你对旧 Context 的所有引用,实际上指向的是新请求的数据。
|
|||
|
|
|
|||
|
|
### 原因三:reset() 会清空一切状态
|
|||
|
|
|
|||
|
|
每次从池中取出后都会调用 `reset()`,覆盖所有字段:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func (c *Context) reset(w http.ResponseWriter) {
|
|||
|
|
c.Writer = w.(*responseWriter) // Writer 被替换
|
|||
|
|
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 // 表单缓存被清空
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
持有着旧 Context 引用会导致:
|
|||
|
|
|
|||
|
|
| 行为 | 后果 |
|
|||
|
|
|------|------|
|
|||
|
|
| 读取 `c.Keys` | 读到下一个请求的 Keys(可能被其他中间件写入) |
|
|||
|
|
| 调用 `c.Param("id")` | 返回空或下一个请求的路径参数 |
|
|||
|
|
| 写入 `c.JSON(...)` | 写入错误的 ResponseWriter,响应混乱 |
|
|||
|
|
| 并发访问同一引用 | 数据竞争,不可预知的崩溃 |
|
|||
|
|
|
|||
|
|
### ✅ 正确做法:只拷贝值,不传递引用
|
|||
|
|
|
|||
|
|
如果需要在 handler 返回后异步使用数据,**提取并拷贝所需的值**:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
func handler(c *gin.Context) {
|
|||
|
|
userID := c.Param("id") // ★ 提取为基本类型
|
|||
|
|
requestID := c.GetString("request_id")
|
|||
|
|
|
|||
|
|
c.JSON(200, gin.H{"ok": true})
|
|||
|
|
|
|||
|
|
// 异步任务:只传值,绝不传 Context 引用
|
|||
|
|
go func(id string, rid string) {
|
|||
|
|
processBackground(id, rid) // 安全:纯值传递
|
|||
|
|
}(userID, requestID)
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**核心原则:** handler 返回前,把所有需要的数据从 Context 中提取出来存为局部变量——这些值是栈上的拷贝,不受 Context 回收影响。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[4-context-lifecycle]] — Context 生命周期总览(父文档)
|
|||
|
|
- [[GIN/1-gin-architecture]] — Engine 初始化与 Context 池化
|