异步 goroutine 中直接传 r.context() 会导致任务被请求取消中断,应使用 context.withoutcancel(r.context()) 剥离取消信号;若需独立超时,须链式调用 context.withtimeout(context.withoutcancel(r.context()), 30*time.second),并显式调用 cancel()。

异步 goroutine 里直接传 r.Context() 会出事
直接把 r.Context() 传进 go func() { ... }(),等于把整个请求生命周期强绑给后台任务。一旦 HTTP 请求结束(比如用户关了页面、Nginx 超时断连),ctx.Done() 立刻关闭,goroutine 就被强制终止——但日志上报、消息投递、缓存刷新这类任务本就不该被请求取消打断。
- 典型现象:后台任务偶发中断,错误日志里频繁出现
context canceled,但业务逻辑其实还没跑完 - 根本原因:没剥离取消信号,只保留了
Value部分 - 正确做法是用
context.WithoutCancel(r.Context())创建新 ctx,它丢弃Donechannel,只继承键值对
需要独立超时?得套两层 context
如果异步任务本身有硬性耗时限制(比如“最多等 30 秒发完消息”),不能只靠 WithoutCancel,得再包一层超时控制。
- 错误写法:
context.WithTimeout(r.Context(), 30*time.Second)—— 还带着原始取消信号,可能被上游提前干掉 - 正确链式调用:
context.WithTimeout(context.WithoutCancel(r.Context()), 30*time.Second) - 必须显式调用
cancel():哪怕用了defer cancel(),也要在任务完成/失败后立刻手动调一次,防止 timer 泄漏
中间件里增强 context 后,别忘了更新 request 对象
很多同学在中间件里调了 context.WithValue(ctx, key, val),以为下游能自动拿到,结果 handler 里 req.Context().Value(key) 返回 nil。
- 根本原因:
r.Context()是只读副本,修改后必须用r = r.WithContext(newCtx)替换原 request - 常见陷阱:只写了
newCtx := context.WithValue(...),却没执行r = r.WithContext(newCtx) - key 类型必须是未导出类型,比如
type traceIDKey struct{},避免字符串 key 冲突覆盖
取值时永远用双返回值判断,别信类型断言
context.Value() 返回 interface{},强制断言如 v.(string) 在值不存在或类型不匹配时直接 panic。
- 安全写法:
if v, ok := ctx.Value(key).(string); ok { ... } - 更推荐封装 getter 函数,比如
func GetTraceID(ctx context.Context) (string, bool),把类型检查收口 - 注意:
context.Value只接受可比较类型(int/string/空 struct),传[]byte或map会在运行时报panic: invalid memory address
真正容易被忽略的是:异步任务里的 context 不是拿来“监听取消”的,而是用来“延续上下文语义”的——trace ID 要透传,用户 ID 要可用,但取消权必须主动剥离。混用 WithCancel 和 WithoutCancel 的边界,比写错超时时间更容易引发线上故障。











