Go context 实战:deadline、取消传播与那些救命的模式
context.Context 是 Go 标准库里讨论最多、误用最多的类型。多数代码只做了最低限度:接住它、传下去、忽略取消信号、什么乱七八糟都塞 context.Value。这是一份能避开所有常见错误的实战指南。
context 真正用来做什么
三件事:
- deadline / timeout 传播 — "调用者最多等 5 秒,超时就别再阻塞了"。
- 取消传播 — "调用者不再关心了,停下来"。
- 请求范围的值 — 跨切面的少量数据(trace ID、登录态、locale),不污染每个函数签名地透过调用栈传递。
context 不是用来:
- 通用依赖注入。
*sql.DB、*log.Logger显式传递。 - 配置。用 options 结构体。
- 携带可变状态。它不是侧通道。
取消 — 正确做法
错:
func doWork() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() // ... 从未把 ctx 传下去 }
对:
func handle(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() result, err := backend.Fetch(ctx, r.URL.Path) if err != nil { http.Error(w, err.Error(), 500) return } w.Write(result) }
两点关键:r.Context() 是父 context(客户端断开时它会被取消),cancel() 一定会执行(defer 保障),即便函数提前 return。
Deadline vs Timeout
ctx, _ := context.WithDeadline(parent, time.Date(2026, 5, 2, 12, 0, 0, 0, time.UTC)) ctx, _ := context.WithTimeout(parent, 500*time.Millisecond)
WithDeadline 是绝对时间,WithTimeout 是"现在 + 时长"。按业务选:
- 用户端 handler 必须按 SLA 响应 →
WithTimeout。 - 批处理必须在某个业务时刻前完成 →
WithDeadline。
是传播,不是包装
接到 context 的函数应原样传给下游,除非你要加更紧的约束:
// 好 —— 给慢调用加更紧 deadline func loadOrderHistory(ctx context.Context, userID string) ([]Order, error) { ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond) defer cancel() return db.QueryContext(ctx, query, userID).Scan(...) } // 坏 —— 覆盖了父 context 的 200ms SLA func loadOrderHistory(ctx context.Context, userID string) ([]Order, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) // ... }
context.Background() 在你的服务里应该恰好出现一次:最顶层,请求边界开始的地方。
context.Value — 把不舒服的 API 用对
Value(key any) any 设计上就别扭。标准库故意让你三思。
type ctxKey string const requestIDKey ctxKey = "request-id" func WithRequestID(ctx context.Context, id string) context.Context { return context.WithValue(ctx, requestIDKey, id) } func RequestID(ctx context.Context) string { if v, ok := ctx.Value(requestIDKey).(string); ok { return v } return "" }
要点:key 用私有类型避免冲突,加 typed accessor。永远这么写——别直接用字符串字面量。
适合放:trace ID、登录用户、locale、tenant ID。不该放:数据库 handle、feature flag client、配置结构体。
监听取消
长跑 goroutine:
for { select { case <-ctx.Done(): return ctx.Err() case work := <-workCh: process(work) } }
ctx.Done() 返回一个 channel,context 被取消时关闭。ctx.Err() 告诉你是 context.Canceled 还是 context.DeadlineExceeded——两者重试策略不同。
goroutine 泄漏模式与修法
错:
func search(query string) <-chan Result { out := make(chan Result) go func() { defer close(out) for r := range slowAPI.Stream(query) { out <- r // 调用方不读就永远阻塞 } }() return out }
调用方一不读,goroutine 永远阻塞在 send,泄漏。
对:
func search(ctx context.Context, query string) <-chan Result { out := make(chan Result) go func() { defer close(out) for r := range slowAPI.Stream(query) { select { case out <- r: case <-ctx.Done(): return } } }() return out }
select 上 ctx.Done() 是安全阀。每个长跑 send/receive 都配一个。
反模式
context.TODO()留在生产代码。 它是占位符,换掉。ctx context.Context不放第一个参数。 约定永远第一个。- 用
time.Sleep而不是select { case <-ctx.Done(): case <-time.After(...): }。 Sleep 忽略取消。 - 客户端断开就 ERROR 级别打 ctx.Err()。
context.Canceled降级到 debug,它是正常的。 - 请求 handler 里给子函数传
context.Background()。 你刚刚切断了取消链。
一句话总结
- 任何做 I/O 或耗时工作的函数都接
ctx context.Context作第一个参数。 - 派生 context 立刻
defer cancel()。 - 原样传父 context,除非要加更紧约束。
context.Value节制使用,typed key,跨切面请求数据用。- 每个长跑 send/receive 配一个
select <-ctx.Done()。
五条做对,省一半生产事故。
相关阅读
Swift 并发 2026:async/await、Actor 与 Sendable 严格检查
务实的 Swift 并发指南 — async/await 用法、Actor 与 @MainActor、结构化并发 TaskGroup、Swift 6 strict Sendable 检查及老回调 API 迁移。
TypeScript 类型体操:什么时候值,什么时候在炫技
务实的 TypeScript 高级类型指南 — mapped types、conditional types、template literal types 真正能给你什么,什么时候用,什么时候应该退回到朴素代码。
Kubernetes 资源 requests / limits 实战:不会把生产搞挂的设法
怎么在生产里实际设 Kubernetes CPU 与内存的 requests/limits — QoS 类、CPU 节流、OOM kill、那些害公司钱的差别,以及好使的模式。