并发编程中,除了"互斥访问"和"等待通知"之外,还有一类核心需求——取消和超时控制。比如:HTTP 请求超时了,下游所有 goroutine 都应该停止工作;用户取消了操作,正在进行的数据库查询应该被中断。Go 标准库的 context 包就是为了解决这类问题而生的。

一、Context 解决什么问题?

假设一个 HTTP 请求触发了多个 goroutine 并行处理:

  HTTP 请求
      ├── goroutine: 查询数据库
      ├── goroutine: 调用下游 API
      └── goroutine: 读取缓存

如果请求超时了(比如客户端断开连接),这三个 goroutine 应该如何知道"不用干了"?

没有 Context:                        有 Context:

  请求超时                              请求超时
      │                                     │
      │  数据库查询还在跑...                │── ctx.Done() 触发
      │  下游 API 还在等...                 │── 所有 goroutine 收到信号
      │  缓存读取还在等...                  │── 立即停止,释放资源
      │                                     │
      └── 资源浪费 💀                       └── 干净退出 ✅

Context 提供了三个核心能力:

┌──────────────────────────────────────────────────────────────┐
│                   Context 的三大能力                          │
├──────────────────────────────────────────────────────────────┤
│  1. 取消信号传播   父 Context 取消 → 所有子 Context 自动取消 │
│  2. 超时/截止时间  到期后自动取消                             │
│  3. 传递请求级数据  trace ID、用户信息等跨 API 边界传递      │
└──────────────────────────────────────────────────────────────┘

二、Context 的接口

context.Context 是一个接口,只有四个方法:

┌──────────────────────────────────────────────────────────────────────┐
│                     context.Context 接口                             │
├──────────────────────────────────────────────────────────────────────┤
│  Deadline() (deadline time.Time, ok bool)                            │
│      返回截止时间(如果设置了的话)                                   │
│                                                                      │
│  Done() <-chan struct{}                                               │
│      返回一个 channel,Context 被取消时关闭                          │
│                                                                      │
│  Err() error                                                         │
│      Done channel 关闭后,返回取消原因                               │
│      • context.Canceled         ← 主动取消                          │
│      • context.DeadlineExceeded ← 超时                               │
│                                                                      │
│  Value(key any) any                                                  │
│      获取与 key 关联的值                                             │
└──────────────────────────────────────────────────────────────────────┘

三、创建 Context 的四种方式

// 1. 根 Context(不可取消,通常作为起点)
ctx := context.Background()  // 主函数、初始化、测试
ctx := context.TODO()        // 不确定用哪个时的占位符

// 2. 可取消的 Context
ctx, cancel := context.WithCancel(parentCtx)
defer cancel() // ✅ 必须调用,释放资源

// 3. 带超时的 Context
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()

// 4. 带截止时间的 Context
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(parentCtx, deadline)
defer cancel()

它们形成一棵树状结构:

  context.Background()
      ├── WithCancel() ──→ ctx1
      │       │
      │       ├── WithTimeout(3s) ──→ ctx2
      │       │
      │       └── WithValue("traceID", "abc") ──→ ctx3
      └── WithTimeout(10s) ──→ ctx4
              └── WithCancel() ──→ ctx5

  取消 ctx1 → ctx2、ctx3 也被取消
  取消 ctx4 → ctx5 也被取消
  取消 ctx5 → ctx4 不受影响(子不影响父)

关键规则:父 Context 取消,所有子 Context 自动取消。子 Context 取消,不影响父 Context。

四、基本用法

超时控制

func fetchData(ctx context.Context) (string, error) {
    // 模拟一个耗时操作
    ch := make(chan string, 1)
    go func() {
        time.Sleep(3 * time.Second) // 模拟耗时
        ch <- "data"
    }()

    select {
    case data := <-ch:
        return data, nil
    case <-ctx.Done():
        return "", ctx.Err() // 超时或被取消
    }
}

func main() {
    // 设置 1 秒超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    data, err := fetchData(ctx)
    if err != nil {
        fmt.Println("失败:", err) // context deadline exceeded
        return
    }
    fmt.Println("成功:", data)
}

取消传播

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    // 启动多个 worker
    for i := 0; i < 5; i++ {
        go worker(ctx, i)
    }

    time.Sleep(2 * time.Second)
    cancel() // 通知所有 worker 停止
    time.Sleep(time.Second) // 等待 worker 退出
}

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d: 收到取消信号,退出\n", id)
            return
        default:
            fmt.Printf("Worker %d: 工作中...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

传递请求级数据

type contextKey string

const traceIDKey contextKey = "traceID"

func withTraceID(ctx context.Context, traceID string) context.Context {
    return context.WithValue(ctx, traceIDKey, traceID)
}

func getTraceID(ctx context.Context) string {
    if v, ok := ctx.Value(traceIDKey).(string); ok {
        return v
    }
    return "unknown"
}

func handleRequest(ctx context.Context) {
    traceID := getTraceID(ctx)
    fmt.Printf("[%s] 处理请求\n", traceID)
    queryDB(ctx)
}

func queryDB(ctx context.Context) {
    traceID := getTraceID(ctx)
    fmt.Printf("[%s] 查询数据库\n", traceID)
}

五、Context 的传递规范

Go 官方对 Context 的使用有明确的规范:

┌──────────────────────────────────────────────────────────────────┐
│  Context 使用规范                                                │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. Context 作为函数的第一个参数,命名为 ctx                     │
│     func DoSomething(ctx context.Context, arg string) error      │
│                                                                  │
│  2. 不要把 Context 放到 struct 中                                │
│     (Go 1.7 的建议;某些长生命周期对象可以例外)                │
│                                                                  │
│  3. 不要传递 nil Context                                         │
│     不确定用什么就用 context.TODO()                               │
│                                                                  │
│  4. WithValue 只传递请求级的数据                                 │
│     trace ID、请求 ID、认证 token 等                             │
│     不要传递业务参数(函数签名应该明确依赖)                     │
│                                                                  │
│  5. 同一个 Context 可以传给多个 goroutine                        │
│     Context 是并发安全的                                         │
│                                                                  │
└──────────────────────────────────────────────────────────────────┘

六、实现原理

cancelCtx

WithCancel 返回的核心类型是 cancelCtx

┌──────────────────────────────────────────────────────────┐
│                    cancelCtx                              │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  Context      ← 父 Context                              │
│  done  chan struct{}   ← Done() 返回这个 channel         │
│  children map[canceler]struct{}  ← 子 Context 列表      │
│  err  error            ← 取消原因                        │
│                                                          │
│  cancel():                                               │
│    1. 关闭 done channel                                  │
│    2. 遍历 children,逐个取消                            │
│    3. 从父 Context 中移除自己                            │
│                                                          │
└──────────────────────────────────────────────────────────┘

timerCtx

WithTimeoutWithDeadline 返回的是 timerCtx,它在 cancelCtx 基础上加了定时器:

┌──────────────────────────────────────────────────────────┐
│                     timerCtx                              │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  cancelCtx   ← 内嵌 cancelCtx                           │
│  timer  *time.Timer  ← 到期后自动触发 cancel            │
│  deadline time.Time  ← 截止时间                          │
│                                                          │
│  到期时:                                                │
│    timer 触发 → 调用 cancelCtx.cancel()                  │
│    → 关闭 done channel                                   │
│    → 取消所有子 Context                                   │
│                                                          │
└──────────────────────────────────────────────────────────┘

valueCtx

WithValue 返回的是 valueCtx,它的查找是链式的:

ctx3 := WithValue(ctx2, "k3", "v3")
ctx2 := WithValue(ctx1, "k2", "v2")
ctx1 := WithValue(bg, "k1", "v1")
bg   := Background()

ctx3.Value("k1") 的查找路径:
  ctx3 → key 不匹配
  ctx2 → key 不匹配
  ctx1 → key 匹配 ✅ 返回 "v1"

注意:Value 的查找是 O(n) 的,n 是 WithValue 的嵌套层数。不要存大量数据,也不要在性能敏感路径上频繁调用。

七、使用 Context 最常踩的 4 个坑

坑 1:忘记调用 cancel

WithCancel/WithTimeout/WithDeadline 返回的 cancel 函数 必须调用,否则会导致资源泄漏:

// ❌ 忘记调用 cancel,导致 goroutine 和 timer 泄漏
func handle() {
    ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
    // cancel 被丢弃了 💀
    doWork(ctx)
}
// ✅ 始终 defer cancel
func handle() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // 即使操作提前完成,也释放 timer 资源
    doWork(ctx)
}

小技巧:即使 Context 会因超时自动取消,也应该 defer cancel()。因为 cancel 会立即释放内部 timer 和子 Context 的关联,不必等到超时。

坑 2:WithValue 的 key 用了内置类型

// ❌ 用 string 做 key,可能和其他包冲突
ctx = context.WithValue(ctx, "userID", 123)
// ✅ 用自定义未导出类型做 key,避免冲突
type contextKey string
const userIDKey contextKey = "userID"
ctx = context.WithValue(ctx, userIDKey, 123)

坑 3:把业务参数塞进 Context

// ❌ 把业务参数放进 Context
ctx = context.WithValue(ctx, "orderID", orderID)
processOrder(ctx) // 函数签名看不出需要 orderID

// ✅ 业务参数用函数参数传递
processOrder(ctx, orderID) // 依赖关系一目了然

WithValue 只适合传递请求级的横切关注点(trace ID、认证信息、请求 ID),不适合传递业务数据。

坑 4:不检查 ctx.Done()

启动的 goroutine 如果不检查 ctx.Done(),取消信号就传不进去:

// ❌ 不检查 Done,cancel 调了也没用
func doWork(ctx context.Context) {
    for i := 0; i < 1000000; i++ {
        heavyComputation(i) // 死干,不看取消信号
    }
}
// ✅ 在循环中定期检查
func doWork(ctx context.Context) {
    for i := 0; i < 1000000; i++ {
        select {
        case <-ctx.Done():
            return // 收到取消信号,退出
        default:
        }
        heavyComputation(i)
    }
}

八、Go 1.21+ 新增 API

Go 1.21 新增了几个实用的 Context 函数:

// WithoutCancel:创建一个不会被父 Context 取消的子 Context
// 适合在父请求结束后还需要继续的后台任务
ctx := context.WithoutCancel(parentCtx)

// AfterFunc:Context 取消时执行回调
stop := context.AfterFunc(ctx, func() {
    fmt.Println("ctx 被取消了,执行清理")
})
// stop() 可以阻止回调执行
// WithCancelCause(Go 1.20+):取消时附带原因
ctx, cancel := context.WithCancelCause(parentCtx)
cancel(fmt.Errorf("用户主动取消")) // 附带原因

// 获取取消原因
err := context.Cause(ctx) // "用户主动取消"

九、实战建议

  1. 所有外部调用都传 Context——HTTP 请求、数据库查询、RPC 调用,都应该接受 Context
  2. 始终 defer cancel()——防止资源泄漏,即使 Context 会自动超时
  3. WithValue 只传请求级数据——trace ID、认证信息,不传业务参数
  4. 长时间操作要检查 ctx.Done()——否则取消信号传不进去
  5. 超时要合理设置——下游超时应该小于上游超时,留出处理和返回的时间
// 超时层级示例
// HTTP handler: 10s
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
defer cancel()

// 数据库查询: 3s(小于 handler 的 10s)
dbCtx, dbCancel := context.WithTimeout(ctx, 3*time.Second)
defer dbCancel()
# 检查 Context 泄漏
go vet ./...
go test -race ./...

Context 是 Go 并发编程的基础设施——它不是一个同步原语,而是一个 信号传播机制。理解它的树状取消传播和 Value 的链式查找,遵循官方的使用规范,你就能在并发程序中优雅地处理超时和取消。