Golang Context

为什么需要 Context

Go 并发编程中,context 包解决三个问题:

  • 如何优雅地取消一串关联的 goroutine?
  • 如何在 goroutine 间安全传递请求范围的数据
  • 如何为操作设置超时/截止时间

它遵循三个设计原则:不可变(修改都会产生新 Context,绝不原地改)、树形传播(父子 Context 的取消向下传导)、接口统一(标准接口,多种实现)。

核心接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)  // 截止时间
    Done() <-chan struct{}                    // 取消信号通道
    Err() error                               // 取消原因
    Value(key any) any                        // 请求范围数据
}

创建与派生

ctx := context.Background()   // 根 Context,通常用于 main/入口
ctx := context.TODO()         // 不确定用什么时占位

// 手动取消
ctx, cancel := context.WithCancel(parent)
defer cancel()                // 一定要调用,否则资源泄漏

// 相对超时
ctx, cancel := context.WithTimeout(parent, 2*time.Second)
defer cancel()

// 绝对截止
ctx, cancel := context.WithDeadline(parent, time.Now().Add(2*time.Second))
defer cancel()

// 携带数据(key 用自定义类型,不要用 string)
type ctxKey string
const userIDKey ctxKey = "userID"
ctx = context.WithValue(ctx, userIDKey, "12345")

实战场景

1. HTTP 超时控制

func handler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()
    result, err := someLongRunningTask(ctx)
    if err != nil { http.Error(w, err.Error(), 500); return }
    w.Write([]byte(result))
}

2. 并发任务取消(errgroup)

g, ctx := errgroup.WithContext(ctx)   // 任一任务出错即取消整个组
for _, task := range tasks {
    g.Go(func() error { return task.Execute(ctx) })
}
if err := g.Wait(); err != nil { /* 处理 */ }

3. 数据库操作超时

ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = ?", userID)

最佳实践

  • Context 作为函数第一个参数,约定俗成;
  • 不要存进结构体:Context 属于调用链,不属于对象状态;
  • key 用自定义类型,避免与其他包冲突(String 做 key 是官方明确反对的);
  • 总是 defer cancel():即使不会主动取消也要调用,否则底层定时器泄漏;
  • 阻塞读用 select 包裹,让取消能被响应。

常见陷阱

  • 忘记 cancel 导致泄漏ctx, _ := context.WithCancel(parent) 在长时间运行的场景会积累资源;
  • 滥用 Value:Value 只用于请求范围数据(request ID、认证信息),别拿来传业务参数;
  • 阻塞不响应取消:裸读 <-ch 无法被取消,应写 select;
  • context 传 nil:库函数收到 nil ctx 会 panic,入口处用 Background 兜底。

内部实现要点

取消机制:cancel 调用时关闭内部 done channel,并递归取消所有子 Context。两个值得一提的优化:done channel 懒加载(第一次调用 Done() 才创建,减少内存分配)和 valueCtx 链式查找(Value 沿父链逐级查找)。

滚动至顶部