go-context

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Go Context

Go Context

context.Context
controls cancellation, deadlines, and request-scoped values across API boundaries. Misusing it causes goroutine leaks, orphaned work, and subtle production bugs.
context.Context
用于跨API边界控制取消操作、截止时间以及请求作用域内的值传递。误用它会导致goroutine泄漏、孤立任务以及难以排查的生产环境Bug。

1. Core Rules

1. 核心规则

Context is always the first parameter:

Context始终作为第一个参数:

go
// ✅ Good — context is first
func GetUser(ctx context.Context, id string) (*User, error)
func (s *Service) Process(ctx context.Context, req Request) error

// ❌ Bad — context buried in the middle or end
func GetUser(id string, ctx context.Context) (*User, error)
func Process(req Request, ctx context.Context) error
go
// ✅ 良好实践 — context作为第一个参数
func GetUser(ctx context.Context, id string) (*User, error)
func (s *Service) Process(ctx context.Context, req Request) error

// ❌ 不良实践 — context被放在参数中间或末尾
func GetUser(id string, ctx context.Context) (*User, error)
func Process(req Request, ctx context.Context) error

NEVER store context in a struct:

绝对不要将context存储在结构体中:

go
// ❌ Bad — context stored in struct
type Server struct {
    ctx    context.Context // NEVER do this
    cancel context.CancelFunc
}

// ✅ Good — pass context through method parameters
func (s *Server) Shutdown(ctx context.Context) error {
    return s.httpServer.Shutdown(ctx)
}
Context represents the lifetime of a single operation, not the lifetime of an object.
go
// ❌ 不良实践 — context存储在结构体中
type Server struct {
    ctx    context.Context // 绝对不要这么做
    cancel context.CancelFunc
}

// ✅ 良好实践 — 通过方法参数传递context
func (s *Server) Shutdown(ctx context.Context) error {
    return s.httpServer.Shutdown(ctx)
}
Context代表单个操作的生命周期,而非对象的生命周期。

NEVER pass nil context:

绝对不要传递nil context:

go
// ❌ Bad
doSomething(nil, data)

// ✅ Good — use context.TODO() if unsure which context to use
doSomething(context.TODO(), data)

// ✅ Good — use context.Background() for top-level/main
doSomething(context.Background(), data)
go
// ❌ 不良实践
doSomething(nil, data)

// ✅ 良好实践 — 不确定使用哪个context时,使用context.TODO()
doSomething(context.TODO(), data)

// ✅ 良好实践 — 顶层/主函数使用context.Background()
doSomething(context.Background(), data)

2. Cancellation

2. 取消操作

Always defer cancel:

始终延迟调用cancel:

go
// ✅ Good — cancel called even if operation succeeds
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()

result, err := longOperation(ctx)
Failing to call cancel leaks resources (timers, goroutines) until the parent context is cancelled.
go
// ✅ 良好实践 — 即使操作成功也会调用cancel
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()

result, err := longOperation(ctx)
如果不调用cancel,会导致资源(定时器、goroutine)泄漏,直到父context被取消。

Use WithCancel for manual cancellation:

使用WithCancel进行手动取消:

go
func (s *Supervisor) Run(ctx context.Context) error {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    g, ctx := errgroup.WithContext(ctx)

    g.Go(func() error { return s.runWorkerA(ctx) })
    g.Go(func() error { return s.runWorkerB(ctx) })

    // If any worker returns an error, errgroup cancels ctx,
    // which signals all other workers to stop.
    return g.Wait()
}
go
func (s *Supervisor) Run(ctx context.Context) error {
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    g, ctx := errgroup.WithContext(ctx)

    g.Go(func() error { return s.runWorkerA(ctx) })
    g.Go(func() error { return s.runWorkerB(ctx) })

    // 如果任意worker返回错误,errgroup会取消ctx,
    // 进而通知所有其他worker停止工作。
    return g.Wait()
}

Check context cancellation in loops:

在循环中检查context是否已取消:

go
// ✅ Good — respects cancellation
func processItems(ctx context.Context, items []Item) error {
    for _, item := range items {
        if err := ctx.Err(); err != nil {
            return fmt.Errorf("processing cancelled: %w", err)
        }
        if err := process(ctx, item); err != nil {
            return fmt.Errorf("process item %s: %w", item.ID, err)
        }
    }
    return nil
}

// ❌ Bad — runs to completion even if cancelled
func processItems(ctx context.Context, items []Item) error {
    for _, item := range items {
        process(ctx, item) // ignores ctx cancellation between items
    }
    return nil
}
go
// ✅ 良好实践 — 响应取消操作
func processItems(ctx context.Context, items []Item) error {
    for _, item := range items {
        if err := ctx.Err(); err != nil {
            return fmt.Errorf("处理已取消: %w", err)
        }
        if err := process(ctx, item); err != nil {
            return fmt.Errorf("处理项 %s: %w", item.ID, err)
        }
    }
    return nil
}

// ❌ 不良实践 — 即使已取消仍会执行到完成
func processItems(ctx context.Context, items []Item) error {
    for _, item := range items {
        process(ctx, item) // 忽略项之间的ctx取消信号
    }
    return nil
}

3. Timeouts and Deadlines

3. 超时与截止时间

WithTimeout for duration-based limits:

使用WithTimeout设置基于时长的限制:

go
func (c *Client) FetchUser(ctx context.Context, id string) (*User, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url+"/users/"+id, nil)
    if err != nil {
        return nil, fmt.Errorf("create request: %w", err)
    }

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("fetch user %s: %w", id, err)
    }
    defer resp.Body.Close()

    // ...
}
go
func (c *Client) FetchUser(ctx context.Context, id string) (*User, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url+"/users/"+id, nil)
    if err != nil {
        return nil, fmt.Errorf("创建请求: %w", err)
    }

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("获取用户 %s: %w", id, err)
    }
    defer resp.Body.Close()

    // ...
}

WithDeadline for absolute time limits:

使用WithDeadline设置基于绝对时间的限制:

go
// Use when coordinating with external deadlines (SLAs, cron windows)
deadline := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
go
// 与外部截止时间(SLA、定时窗口)协同工作时使用
deadline := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()

Timeout budgets — don't exceed parent timeout:

超时预算 — 不要超过父context的超时时间:

go
// ✅ Good — child timeout shorter than parent
func handler(ctx context.Context) error {
    // Parent has 30s timeout (from HTTP server)

    // Give DB query 5s of the 30s budget
    dbCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    data, err := db.QueryContext(dbCtx, query)

    // Give external API 10s of the remaining budget
    apiCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    result, err := client.Call(apiCtx, data)

    return nil
}

// ❌ Bad — child timeout exceeds parent (silently capped anyway)
ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second) // parent has 5s left
// This timeout is 60s but will actually fire at parent's deadline
go
// ✅ 良好实践 — 子超时时间短于父超时
func handler(ctx context.Context) error {
    // 父context有30秒超时(来自HTTP服务器)

    // 给数据库查询分配30秒预算中的5秒
    dbCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    data, err := db.QueryContext(dbCtx, query)

    // 给外部API分配剩余预算中的10秒
    apiCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()
    result, err := client.Call(apiCtx, data)

    return nil
}

// ❌ 不良实践 — 子超时时间超过父超时(实际会被父截止时间覆盖)
ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second) // 父context仅剩5秒
// 这个60秒的超时实际会在父context的截止时间触发

Check if deadline exists:

检查是否存在截止时间:

go
if deadline, ok := ctx.Deadline(); ok {
    remaining := time.Until(deadline)
    if remaining < minRequired {
        return fmt.Errorf("insufficient time remaining: %v", remaining)
    }
}
go
if deadline, ok := ctx.Deadline(); ok {
    remaining := time.Until(deadline)
    if remaining < minRequired {
        return fmt.Errorf("剩余时间不足: %v", remaining)
    }
}

4. Context Values

4. Context值传递

Use sparingly — only for request-scoped metadata:

谨慎使用 — 仅用于请求作用域内的元数据:

go
// ✅ Appropriate uses:
// - Request ID
// - Trace/span ID
// - Authenticated user info
// - Request-scoped logger

// ❌ Bad uses:
// - Database connections (use dependency injection)
// - Configuration (use struct fields)
// - Function parameters (pass explicitly)
go
// ✅ 合适的使用场景:
// - 请求ID
// - 追踪/链路ID
// - 已认证用户信息
// - 请求作用域内的日志器

// ❌ 不合适的使用场景:
// - 数据库连接(使用依赖注入)
// - 配置(使用结构体字段)
// - 函数参数(显式传递)

Use unexported key types to prevent collisions:

使用未导出的键类型避免冲突:

go
// ✅ Good — unexported type prevents key collisions
type contextKey struct{}

var requestIDKey = contextKey{}

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
}
go
// ❌ Bad — string keys risk collisions across packages
ctx = context.WithValue(ctx, "request_id", id) // any package could overwrite this
go
// ✅ 良好实践 — 未导出类型防止键冲突
type contextKey struct{}

var requestIDKey = contextKey{}

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
}
go
// ❌ 不良实践 — 字符串键存在跨包冲突风险
ctx = context.WithValue(ctx, "request_id", id) // 任何包都可能覆盖这个值

Always provide accessor functions — never expose the key:

始终提供访问器函数 — 绝不暴露键:

go
// ✅ Good — clean API with accessors
rid := middleware.RequestID(ctx)

// ❌ Bad — exposes internal key type
rid := ctx.Value(requestIDKey).(string) // caller needs key, risks panic on nil
go
// ✅ 良好实践 — 简洁的API访问方式
rid := middleware.RequestID(ctx)

// ❌ 不良实践 — 暴露内部键类型
rid := ctx.Value(requestIDKey).(string) // 调用者需要知道键,存在nil恐慌风险

5. Context in HTTP Handlers

5. HTTP处理器中的Context

Use r.Context() for the request context:

使用r.Context()作为请求上下文:

go
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // carries cancellation when client disconnects

    user, err := h.service.GetUser(ctx, id)
    if err != nil {
        if errors.Is(err, context.Canceled) {
            return // client disconnected, no point writing response
        }
        // handle error...
    }
    // ...
}
go
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // 客户端断开连接时会携带取消信号

    user, err := h.service.GetUser(ctx, id)
    if err != nil {
        if errors.Is(err, context.Canceled) {
            return // 客户端已断开,无需返回响应
        }
        // 处理错误...
    }
    // ...
}

Attach values via middleware:

通过中间件附加值:

go
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, err := authenticate(r)
        if err != nil {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        ctx := WithUser(r.Context(), user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
go
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, err := authenticate(r)
        if err != nil {
            http.Error(w, "未授权", http.StatusUnauthorized)
            return
        }
        ctx := WithUser(r.Context(), user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

6. Context in Testing

6. 测试中的Context

Use context with timeout in tests to prevent hangs:

在测试中使用带超时的context防止挂起:

go
func TestSlowOperation(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    result, err := slowOperation(ctx)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    // assert result...
}
go
func TestSlowOperation(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    result, err := slowOperation(ctx)
    if err != nil {
        t.Fatalf("意外错误: %v", err)
    }
    // 断言结果...
}

Test cancellation behavior:

测试取消行为:

go
func TestCancellation(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    cancel() // cancel immediately

    _, err := operation(ctx)
    if !errors.Is(err, context.Canceled) {
        t.Errorf("expected context.Canceled, got %v", err)
    }
}
go
func TestCancellation(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    cancel() // 立即取消

    _, err := operation(ctx)
    if !errors.Is(err, context.Canceled) {
        t.Errorf("预期context.Canceled,实际得到%v", err)
    }
}

7. context.Background() vs context.TODO()

7. context.Background() vs context.TODO()

FunctionWhen to use
context.Background()
Top-level:
main()
,
init()
, test setup. Intentional root context.
context.TODO()
Placeholder when you don't know which context to use yet. Signals "this needs to be fixed".
context.TODO()
is a code smell in production code — replace it before shipping.
函数使用场景
context.Background()
顶层场景:
main()
init()
、测试初始化。明确的根context。
context.TODO()
暂时不知道使用哪个context时的占位符。表示「此处需要修复」。
context.TODO()
在生产代码中是一种代码异味 — 上线前需替换。

Verification Checklist

验证检查清单

  1. context.Context
    is the first parameter in all functions that accept it
  2. No context stored in struct fields
  3. defer cancel()
    called immediately after
    WithCancel
    ,
    WithTimeout
    ,
    WithDeadline
  4. Long loops check
    ctx.Err()
    between iterations
  5. Child timeouts don't exceed parent timeout budget
  6. Context values use unexported key types with accessor functions
  7. Only request-scoped metadata stored in context values (not configs, connections)
  8. HTTP handlers use
    r.Context()
    and pass it downstream
  9. No
    nil
    context passed — use
    context.TODO()
    or
    context.Background()
  10. Tests use
    context.WithTimeout
    to prevent hanging
  1. 所有接收context的函数都将
    context.Context
    作为第一个参数
  2. 没有context被存储在结构体字段中
  3. 调用
    WithCancel
    WithTimeout
    WithDeadline
    后立即执行
    defer cancel()
  4. 长循环会在每次迭代间检查
    ctx.Err()
  5. 子超时时间不超过父超时预算
  6. Context值使用未导出键类型并提供访问器函数
  7. Context值仅存储请求作用域内的元数据(而非配置、连接)
  8. HTTP处理器使用
    r.Context()
    并向下游传递
  9. 没有传递
    nil
    context — 使用
    context.TODO()
    context.Background()
  10. 测试使用
    context.WithTimeout
    防止挂起