Files
OpenFlare/.agents/skills/go-context/references/PATTERNS.md
T
2026-08-04 11:40:32 +08:00

5.7 KiB
Raw Blame History

Context 模式

派生、检查和传播 context.Context 的常见模式。


Context 不可变性

Context 是不可变的。将同一个 ctx 传递给共享相同截止时间、取消信号、凭据和父级追踪的多个调用是安全的:

// 安全:同一个 context 传递给顺序调用
func ProcessBatch(ctx context.Context, items []Item) error {
    for _, item := range items {
        if err := process(ctx, item); err != nil {
            return err
        }
    }
    return nil
}

// 安全:同一个 context 传递给并发调用
func ProcessConcurrently(ctx context.Context, a, b *Data) error {
    g, ctx := errgroup.WithContext(ctx)
    g.Go(func() error { return processA(ctx, a) })
    g.Go(func() error { return processB(ctx, b) })
    return g.Wait()
}

何时使用 context.Background()

仅在从不特定于请求的函数中使用 context.Background():

func main() {
    ctx := context.Background()
    if err := run(ctx); err != nil {
        log.Fatal(err)
    }
}

func startBackgroundWorker() {
    ctx := context.Background()
    go worker(ctx)
}

默认传递 Context,即使你认为不需要。只有在有充分理由说明传递 context 是错误做法时,才直接使用 context.Background():

func LoadConfig(ctx context.Context) (*Config, error) {
    // 即使现在不使用 ctx,接受它可以在未来添加功能时
    // 不需要修改 API
}

派生 Context

// 添加超时 — 持续时间结束后触发取消
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

// 添加取消 — 调用者控制何时取消
ctx, cancel := context.WithCancel(ctx)
defer cancel()

// 添加截止时间 — 在指定的墙钟时间触发取消
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Hour))
defer cancel()

// 添加值(谨慎使用 — 仅用于请求作用域数据)
ctx = context.WithValue(ctx, requestIDKey, reqID)

创建派生 context 后,始终立即 defer cancel()。这确保即使函数提前返回,资源也会被释放。

嵌套派生

派生的 context 形成树状结构。取消父级会取消所有子级:

func handleRequest(ctx context.Context) error {
    // 整个请求的父级超时
    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    // 数据库调用的更短超时
    dbCtx, dbCancel := context.WithTimeout(ctx, 5*time.Second)
    defer dbCancel()

    data, err := queryDB(dbCtx)
    if err != nil {
        return err
    }

    // 父级 context 的剩余时间适用于此处
    return sendResponse(ctx, data)
}

检查取消

在长时间运行的循环中

func LongRunningOperation(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // 执行工作
        }
    }
}

在高开销操作之前

在开始无法中断的工作之前检查取消:

func ProcessItems(ctx context.Context, items []Item) error {
    for _, item := range items {
        if ctx.Err() != nil {
            return ctx.Err()
        }
        if err := expensiveProcess(item); err != nil {
            return err
        }
    }
    return nil
}

区分取消原因

if err := ctx.Err(); err != nil {
    switch {
    case errors.Is(err, context.Canceled):
        // 调用者显式取消(例如客户端断开连接)
    case errors.Is(err, context.DeadlineExceeded):
        // 超时或截止时间已过
    }
}

在 HTTP 处理器中遵守取消

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    result, err := slowOperation(ctx)
    if err != nil {
        if errors.Is(err, context.Canceled) {
            // 客户端已断开连接 — 无需写入
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(result)
}

r.Context() 在以下情况被取消:

  • 客户端关闭连接
  • http.Server 的 ReadTimeout 或 WriteTimeout 触发
  • ServeHTTP 方法返回

Context 值的最佳实践

使用未导出的键类型

type contextKey struct{}

var userIDKey contextKey

func WithUserID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, userIDKey, id)
}

func UserIDFromContext(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(userIDKey).(string)
    return id, ok
}

使用未导出的结构体类型作为键可以防止与其他包的键发生冲突 — 即使它们使用相同的 string 或 int 值。

提供访问器函数

始终将 context.WithValue 和 ctx.Value 包装在类型化的辅助函数中(如上所示),而不是暴露键。这提供了类型安全性和一个可以修改实现的单一位置。


快速参考

模式 指导
参数位置 始终第一个:func F(ctx context.Context, ...)
结构体存储 不要存储在结构体中;传递给方法
自定义类型 不要创建;使用 context.Context 接口
应用数据 优先选择 参数 > 接收者 > 全局变量 > context 值
请求作用域数据 适用于 context 值
共享 context 安全 — context 是不可变的
context.Background() 仅用于非请求特定的代码
默认行为 即使认为不需要也要传递 context
defer cancel() 在 WithTimeout/WithCancel/WithDeadline 之后始终立即 defer
值键 使用未导出的结构体类型,提供访问器函数
取消检查 在高开销操作前使用 ctx.Err();在循环中使用 select 监听 ctx.Done()