Context不可复用,必须通过WithCancel/WithTimeout等函数从父Context派生新实例;goroutine中应使用派生ctx而非原始ctx;gin.Context不可直接传入goroutine,需用c.Request.Context()派生;WithValue的key须为unexported类型。

Context 不能“复用”,只能派生
Context 不是可复用的资源,而是树状传播的信号载体。所谓“复用”在 Go 的 context 设计中本身就是错误概念 —— 你不能把一个已 cancel 或已超时的 ctx 拿来再用,也不能跨请求缓存 ctx 实例。每次需要新控制逻辑(如新超时、新取消点、新键值),都必须调用 WithCancel、WithTimeout 等函数从父 Context 派生新实例。
常见误操作包括:
- 把 handler 中收到的
r.Context()存进全局变量或 sync.Pool,下次请求直接拿出来用 → panic 或信号错乱 - 在 for 循环里反复传同一个
ctx给多个 goroutine,却不派生 → 所有 goroutine 共享同一取消信号,无法独立控制 - 用
context.TODO()占位后忘记替换,导致下游 HTTP 请求、DB 查询完全不响应超时
goroutine 中必须用派生 ctx,而非原始 ctx
当你启动多个子 goroutine 并行执行不同任务(如查 DB、调第三方 API、发消息),每个任务应有自己的派生 ctx,否则一个任务提前失败 cancel,其他任务也会被连带终止,违背并行初衷。
正确做法是为每个任务单独派生:
立即学习“go语言免费学习笔记(深入)”;
parentCtx := r.Context()
g, _ := errgroup.WithContext(parentCtx)
for _, item := range items {
// 每个 item 使用独立派生 ctx,避免互相干扰
itemCtx, cancel := context.WithTimeout(parentCtx, 3*time.Second)
defer cancel() // 注意:这里 defer 属于外层循环作用域,实际应移入 goroutine 内部或改用显式 cancel
g.Go(func() error {
defer cancel() // ✅ 在 goroutine 内部 defer,确保只 cancel 自己这条链
return doWork(itemCtx, item)
})
}
关键点:
-
WithTimeout/WithCancel返回的新ctx是线程安全的,可并发传给多个 goroutine - 但对应的
cancel函数**只能由派生方调用一次**,且不应跨 goroutine 共享调用 - 若任务间需协同取消(如任一失败则全部停止),才共用同一个派生
ctx;否则务必分离
gin.Context 里的 context 不能直接传给 goroutine
这是 Gin 用户最常踩的坑:c 是 *gin.Context,它内部封装了 http.Request 和其 req.Context(),但 c 本身不是纯 context —— 它绑定了响应 writer、参数解析状态等生命周期敏感字段。
直接把 c 传进 goroutine 并调 c.JSON() 或 c.MustGet(),会在请求结束后触发 reflect: call of reflect.Value.Type on zero Value panic。
安全方案只有两种:
- 仅需读取数据:提取值再传,例如
userID := c.GetInt("user_id"),然后传userID进 goroutine - 需要完整上下文信号:用
c.Request.Context()拿到标准context.Context,再派生(如加 timeout),**绝不传*gin.Context实例本身**
错误示例:
go func() {
c.JSON(200, "done") // ❌ 请求已结束,c.Writer 已失效
}()
正确示例:
reqCtx := c.Request.Context()
workCtx, cancel := context.WithTimeout(reqCtx, 5*time.Second)
defer cancel()
go func() {
defer cancel()
result := heavyWork(workCtx) // 只用 workCtx 控制生命周期
log.Printf("result: %v", result)
}()
WithValue 的 key 必须是 unexported 类型,且不可写入 map
用 context.WithValue 传请求级数据(如 trace_id、user_info)很常见,但极易因 key 类型不当引发静默失效。
典型问题:
- 用字符串字面量当 key:
ctx = context.WithValue(ctx, "trace_id", id)→ 不同包里同名字符串不相等,ctx.Value("trace_id")返回 nil - 用 public struct 字段当 key → 跨包无法比较,且可能被意外修改
- 把整个
map或struct塞进 Value → 并发读写 panic(context.Value 不提供同步保障)
正确方式:
type ctxKey string
const userKey ctxKey = "user"
// 设置
ctx = context.WithValue(ctx, userKey, &User{ID: 123})
// 获取(类型断言安全)
if u, ok := ctx.Value(userKey).(*User); ok {
// use u
}
注意:WithValue 仅用于传递**不可变的请求元数据**,不要用来传状态、缓存或可变对象。需要共享状态请用 channel 或 sync.Map。


















