必须手动调用cancel(),因为超时仅关闭Done通道并设置Err,但内部time.Timer未停止、goroutine持续运行,不调用则timer和关联资源无法释放,导致泄漏。

context.WithTimeout 为什么超时后还要手动调用 cancel()
即使 context.WithTimeout 已自动触发取消,仍必须显式调用返回的 cancel() 函数,否则会泄漏底层定时器和 goroutine。
原因在于:超时只是让 Done() 通道关闭、Err() 返回 context.DeadlineExceeded,但内部的 time.Timer 仍在运行,且其 goroutine 不会自动退出。不调用 cancel(),该 timer 就一直持有引用,无法被 GC 回收。
常见错误写法:
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second) // 忘记接收 cancel // ... 启动任务 // 缺少 defer cancel() → 定时器泄露
正确做法始终是:
立即学习“go语言免费学习笔记(深入)”;
- 用两个变量接收:
ctx, cancel := context.WithTimeout(...) - 在作用域末尾加
defer cancel()(哪怕已超时) - 若需提前取消(如任务提前完成),也应调用
cancel(),而非依赖超时
监听 ctx.Done() 时为什么不能只靠 if 判断
ctx.Done() 是一个只读 channel,一旦关闭就永远关闭;但直接 if ctx.Done() != nil 或 if ctx.Err() != nil 是无效的——因为 ctx.Err() 在取消前返回 nil,而 ctx.Done() 永远非 nil(它是个有效 channel,只是未关闭)。
真正安全的监听方式只有 select:
select {
case <-ctx.Done():
// 此时可安全调用 ctx.Err()
log.Println("exit due to:", ctx.Err())
return
default:
// 继续工作
}
常见误用场景:
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 在循环里反复
if ctx.Err() != nil→ 永远拿不到错误,直到超时后才突变一次,错过响应时机 - 用
for ctx.Err() == nil { ... }→ 逻辑等价于无限循环,因为ctx.Err()在取消前恒为nil - 对非阻塞 I/O(如
http.Client.Do)忽略select,只等函数返回 → 失去主动中断能力
WithValue 的 key 为什么不能用 string
context.WithValue 要求 key 是可比较类型,但用 string 作 key 极易引发冲突:不同包、不同模块可能无意中用了相同字符串 key,导致值被意外覆盖或读错。
比如两个独立库都用 "user_id" 存用户 ID,但类型不同(int vs string),或语义不同(登录用户 vs 请求发起者),就会互相干扰。
安全做法是定义私有未导出类型:
type userIDKey struct{}
ctx = context.WithValue(ctx, userIDKey{}, 123)
这样 key 具备唯一性,编译期就能防止误用。同时注意:
- 值应是只读的;若需修改,请用外部同步机制(如
sync.Mutex) - 绝不传函数、接口、切片、map 等可变对象——
WithValue不提供线程安全保证 - 业务参数优先走函数参数,
WithValue只用于请求级元数据(traceID、auth token、租户标识)
HTTP 请求中 context 传递失败的典型断点
很多 HTTP 超时没生效,不是 context 创建错了,而是传递链断在某个环节。最常出问题的三个位置:
- 调用
http.NewRequest而非http.NewRequestWithContext→ 新建的 request 没绑定上下文,客户端完全无视 timeout - 自定义
http.RoundTripper或中间件中没透传req.Context()→ 比如日志中间件重写了 request,却忘了把原 context 带过去 - 下游服务调用(如 gRPC、数据库驱动)未接收
ctx参数 → 例如用db.Query("...")而非db.QueryContext(ctx, "..."),超时信号到此为止
验证方法:在 handler 开头打印 ctx.Deadline(),再在 DB 查询前打印一次,如果后者变成 ok=false,说明 context 没传下去。
真正健壮的链路,是从 http.Request.Context() 开始,每一层都显式接收并向下传递 ctx,中间不新建、不丢弃、不混用 Background()。

















