Skip to content
Go
context — 上下文传递与取消

context — 上下文传递与取消

context 是 Go 并发编程中使用最广、误解最深的包。它在 goroutine 之间传递:截止时间、取消信号和请求范围的值


1. 四种创建方式

// 根 context(通常在整个程序入口使用)
ctx := context.Background()    // 用于 main、init、测试
ctx := context.TODO()          // 占位符:表示"还没想好用哪种 " 

// 派生的 context
ctx, cancel := context.WithCancel(parent)           // 手动取消
ctx, cancel := context.WithTimeout(parent, 5*time.Second)  // 绝对超时
ctx, cancel := context.WithDeadline(parent, deadline)       // 截止时间
ctx := context.WithValue(parent, key, value)                // 携带值

🚨 顶级陷阱WithCancel/WithTimeout/WithDeadline 返回的 cancel 函数必须被调用,否则会 goroutine 泄漏。Go 官方明确说:“即使 ctx 已经超时自动取消,调用 cancel 仍然是安全且推荐的。“始终用 defer cancel()


2. 取消传播机制

2.1 Parent-Child 取消链

parentCtx, cancel := context.WithCancel(context.Background())
defer cancel()

childCtx, _ := context.WithCancel(parentCtx)

// 取消 parent → child 也被取消
// 取消 child → parent 不受影响
cancel() // parent 和 child 都收到 Done() signal

2.2 🔬 深入原理:ctx.Done() 的工作原理

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():  // ← 返回一个只读 channel
            fmt.Println("cancelled:", ctx.Err())
            return
        default:
            doWork()
        }
    }
}
  • ctx.Done() 返回的 channel 永不发送数据,只是被关闭。
  • 关闭的 channel 立即可读(零值),所以所有阻塞在 <-ctx.Done() 上的 goroutine 同时被唤醒。
  • ctx.Err() 返回取消原因:context.Canceled(手动取消)或 context.DeadlineExceeded(超时)。

2.3 正确使用 select

// ✅ 正确 — 检查 ctx.Done() 做分支
select {
case <-ctx.Done():
    return ctx.Err()
case result := <-workCh:
    return process(result)
}

// ❌ 错误 — 只是在 select 中使用 time.After
select {
case <-time.After(timeout):
    return ErrTimeout
case result := <-workCh:
    return process(result)
}
// 问题:不会响应上层传递的取消信号。如果 ctx 被取消,这个 goroutine 仍然阻塞。

3. Context 传递值

3.1 规范做法

// 1. 定义私有 key 类型(避免冲突)
type contextKey string  // 或用 struct{} 更安全

const (
    requestIDKey contextKey = "requestID"
    userIDKey    contextKey = "userID"
)

// 2. 封装存取函数
func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey, id)
}

func RequestID(ctx context.Context) string {
    id, _ := ctx.Value(requestIDKey).(string)
    return id
}

🚨 陷阱

  • 不要用 string 直接当 key。不同包可能使用相同的 key 字符串,造成覆盖。使用自定义类型(type myKey struct{})作为 key。
  • 不要用 Context 传业务参数。Context 只适合传请求范围的元数据(request ID、trace ID、认证信息),不适合传函数参数。

3.2 Context 查找链

// WithValue 在父 ctx 基础上添加键值对(不可变设计)
ctx := context.Background()
ctx = context.WithValue(ctx, key1, "val1")
ctx = context.WithValue(ctx, key2, "val2")

// 查找链:ctx → key2(不匹配)→ 父 ctx → key1(匹配)
// 时间复杂度:O(depth),不建议嵌套太深

4. 超时控制的经典模式

4.1 HTTP 请求超时

func fetchURL(ctx context.Context, url string) ([]byte, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}

// 调用方设置总体超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
data, err := fetchURL(ctx, "https://example.com")

4.2 数据库查询超时

func queryUsers(ctx context.Context, db *sql.DB, name string) ([]User, error) {
    // 为单个查询再设置更短的超时
    queryCtx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
    defer cancel()
    return db.QueryContext(queryCtx, "SELECT * FROM users WHERE name = ?", name)
}

4.3 串行操作 vs 并行操作

// 串行:每个操作独立超时
func serialWork(ctx context.Context) error {
    // 操作 A: 最多 1s
    ctxA, cancelA := context.WithTimeout(ctx, time.Second)
    defer cancelA()
    if err := doA(ctxA); err != nil {
        return err
    }

    // 操作 B: 最多 2s
    ctxB, cancelB := context.WithTimeout(ctx, 2*time.Second)
    defer cancelB()
    return doB(ctxB)
}

// 并行:同时启动,等任意完成或全部超时
func parallelWork(ctx context.Context) error {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    ch := make(chan error, 2)
    go func() { ch <- doA(ctx) }()
    go func() { ch <- doB(ctx) }()

    for i := 0; i < 2; i++ {
        if err := <-ch; err != nil {
            return err // ← 隐式触发 cancel(),另一个 goroutine 被取消
        }
    }
    return nil
}

5. 🚨 Context 使用规范(官方推荐)

  1. 不要存储在结构体中。Context 应该作为函数第一个参数传递(通常是 ctx)。
  2. Context 参数总在第一位func DoSomething(ctx context.Context, arg string) error
  3. 不要传 nil。如果不确定用什么,用 context.TODO()
  4. 不要将 Context 用于传递可选参数。用 struct 或 options 模式。
  5. ctx.Value 仅用于请求范围的跨 API 边界数据
  6. 始终调用 cancel 函数。即使 ctx 已自动取消。
  7. Context 是并发安全的。可以在多个 goroutine 中同时使用。

6. 高级:自定义 Context 包装

// 为 Context 添加默认超时的包装器
type timeoutContext struct {
    context.Context
}

func (c *timeoutContext) Deadline() (time.Time, bool) {
    d, ok := c.Context.Deadline()
    if !ok {
        return time.Now().Add(30 * time.Second), true
    }
    return d, true
}

func WithDefaultTimeout(parent context.Context) context.Context {
    return &timeoutContext{parent}
}