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
    防止挂起