Go 服务里 context.Context 的几个实用约定
在写 Kubernetes controller、HTTP API 或者后台 worker 时,context.Context 几乎无处不在。它本身 API 很小,但用错的地方很常见:超时没传下去、goroutine 泄漏、或者把 context 当成「万能参数包」乱塞值。这篇笔记整理几个我在项目里反复用到的约定,不追求完整,只求日常够用。
1. 谁创建,谁负责取消
规则:在请求入口(HTTP handler、gRPC interceptor、reconcile 顶层)用 context.WithTimeout / WithCancel 创建带截止时间的 context,并保证函数返回前调用 cancel()。
1 | func (s *Server) handle(w http.ResponseWriter, r *http.Request) { |
子调用只接收 ctx,不要再套一层无意义的 context.Background()。否则上游取消或超时无法传播,客户端已经断开连接,后台还在打数据库。
2. 不要把 context 存进 struct
Context 应该沿调用链向下传,而不是作为字段挂在 Service、Repository 上。否则:
- 生命周期和某次请求绑定,struct 却长期存活 → 容易用到已取消的 context;
- 测试时很难注入独立的 deadline。
更稳妥的写法是每个方法第一个参数是 ctx context.Context,和 database/sql 的惯例一致。
3. 用 context 传请求范围的数据,别当配置中心
context.WithValue 适合放 trace ID、认证 principal、租户 ID 这类与单次请求同生共死的元数据。配置项、连接池、logger 应通过构造函数或依赖注入传入,不要塞进 context。
若必须用 value,建议用未导出的类型做 key,避免和其他库冲突:
1 | type ctxKey int |
4. 阻塞操作要尊重 ctx.Done()
读库、调 HTTP、等 channel 时,优先选支持 context 的 API(http.NewRequestWithContext、Select 配合 ctx.Done())。手写循环里定期检查:
1 | select { |
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 | func TestFoo(t *testing.T) { |
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)




