理解 Go context:取消、超时与请求级值传递

context 不是万能钥匙,却是请求生命周期里最可靠的约定。用对了,链路干净;用错了,调试会很难受。

在 Go 服务端代码里,context.Context 几乎无处不在:HTTP handler、RPC 中间件、数据库驱动都认它。它的职责很克制——传递取消信号、截止时间,以及少量请求级元数据。

三件事,只做这三件

  1. 取消:上游说停,下游应尽快停。
  2. 超时 / 截止时间:到点自动取消,避免资源泄漏。
  3. 请求级值:如 trace id,且应是不可变的、跨边界稳定的。
不要把 context 当成依赖注入容器。业务配置、logger 实例、数据库连接池,更适合显式参数或结构体字段。

取消树

子 context 被取消时,其所有派生 context 也会被取消;父取消会级联到子。反过来,子取消不会取消父——这是做「局部超时」的基础。

ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()

select {
case <-ctx.Done():
    return ctx.Err()
case res := <-doWork(ctx):
    return res, nil
}

Value 的边界

context.WithValue 适合:请求 ID、认证后的用户标识(若框架约定如此)、分布式追踪 baggage。不适合:可变状态、大对象、可选业务参数。

键应使用包内未导出类型,避免碰撞:

type ctxKey struct{}

func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, ctxKey{}, id)
}

几个容易踩的坑

  • 忘记 defer cancel(),导致定时器与 goroutine 泄漏。
  • 在已取消的 context 上继续发起重操作,却不检查 ctx.Err()
  • context.Background() 塞进本应继承请求 ctx 的深层调用。
  • 在库 API 中把 context 放在参数列表中间——惯用法是第一个参数。

小结

把 context 当作「这条调用链还能活多久」的合约,而不是杂物袋。取消要可观测,超时要显式,Value 要克制——服务会因此更可预测。