Init
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
---
|
||||
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 池化
|
||||
Reference in New Issue
Block a user