在写 Kubernetes controller、HTTP API 或者后台 worker 时,context.Context 几乎无处不在。它本身 API 很小,但用错的地方很常见:超时没传下去、goroutine 泄漏、或者把 context 当成「万能参数包」乱塞值。这篇笔记整理几个我在项目里反复用到的约定,不追求完整,只求日常够用。

1. 谁创建,谁负责取消

规则:在请求入口(HTTP handler、gRPC interceptor、reconcile 顶层)用 context.WithTimeout / WithCancel 创建带截止时间的 context,并保证函数返回前调用 cancel()

1
2
3
4
5
6
7
func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
if err := s.svc.DoWork(ctx); err != nil {
// ...
}
}

子调用只接收 ctx,不要再套一层无意义的 context.Background()。否则上游取消或超时无法传播,客户端已经断开连接,后台还在打数据库。

2. 不要把 context 存进 struct

Context 应该沿调用链向下传,而不是作为字段挂在 ServiceRepository 上。否则:

  • 生命周期和某次请求绑定,struct 却长期存活 → 容易用到已取消的 context;
  • 测试时很难注入独立的 deadline。

更稳妥的写法是每个方法第一个参数是 ctx context.Context,和 database/sql 的惯例一致。

3. 用 context 传请求范围的数据,别当配置中心

context.WithValue 适合放 trace ID、认证 principal、租户 ID 这类与单次请求同生共死的元数据。配置项、连接池、logger 应通过构造函数或依赖注入传入,不要塞进 context。

若必须用 value,建议用未导出的类型做 key,避免和其他库冲突:

1
2
3
4
5
6
type ctxKey int
const userKey ctxKey = 0

func WithUser(ctx context.Context, u string) context.Context {
return context.WithValue(ctx, userKey, u)
}

4. 阻塞操作要尊重 ctx.Done()

读库、调 HTTP、等 channel 时,优先选支持 context 的 API(http.NewRequestWithContextSelect 配合 ctx.Done())。手写循环里定期检查:

1
2
3
4
5
6
select {
case <-ctx.Done():
return ctx.Err()
case result := <-ch:
return handle(result)
}

ctx.Err() 在超时场景下是 context.DeadlineExceeded,取消则是 context.Canceled,日志里区分两者有助于排障。

5. 不要在无关的 goroutine 里随便用请求的 ctx

启动「与本次请求无关」的后台任务时,用 context.Background() 派生子 context,并自己管理生命周期;不要把 HTTP 请求的 ctx 直接丢进长期运行的 goroutine——请求结束 ctx 会被取消,后台任务会莫名其妙失败。

若既要后台跑又要能随进程退出,常用模式是应用级 context.WithCancel(context.Background()),在 main 收到 SIGTERM 时统一 cancel()

6. 测试里用 context 控制超时

表驱动测试里给每个 case 单独的 deadline,避免某个 case 卡死拖垮整个包:

1
2
3
4
5
6
7
func TestFoo(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := Foo(ctx); err != nil {
t.Fatal(err)
}
}

testing.T 在 Go 1.24+ 有 t.Context(),可以和标准库的 context 生态对齐,减少手写 Background()

小结

场景 建议
HTTP / gRPC 入口 WithTimeout + defer cancel()
业务 / 存储方法 func(ctx context.Context, ...)
跨请求配置 依赖注入,不用 WithValue
后台 goroutine 独立 context,别复用请求 ctx
阻塞 I/O 监听 ctx.Done()

context 的设计目标就是取消与截止时间传播。守住这条主线,代码会清爽很多;其余花哨用法,多半可以删掉。


封面:Bing 每日壁纸 · 四川茶园(2026-05-21)