Go context.Context:超时 / 取消 / 传值的正确姿势

context.Context 是 Go 标准库里跨 API 边界传递取消信号 + 超时 + 请求值的
统一方式。所有"可能长时间运行 + 可能需要被取消"的函数都该接 ctx 作为
第一个参数

1. 基础

ctx, cancel := context.WithCancel(context.Background())
defer cancel()    // 必须 defer cancel 避免 goroutine 泄露

go doWork(ctx)

time.Sleep(2 * time.Second)
cancel()    // 显式取消

cancel() 触发后所有继承这个 ctx 的下游都收到信号。

2. 超时

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result, err := http.NewRequestWithContext(ctx, "GET", url, nil)
// 5 秒后自动 cancel;HTTP 客户端会立即中断请求

WithTimeoutWithDeadline(time.Now().Add(d)) 的简写。

3. 在函数里响应 ctx

func doWork(ctx context.Context) error {
    for i := 0; i < 100; i++ {
        select {
        case <-ctx.Done():
            return ctx.Err()   // context.Canceled 或 context.DeadlineExceeded
        case <-time.After(100 * time.Millisecond):
            process(i)
        }
    }
    return nil
}

关键模式:每次能 block 的地方都 select { case <-ctx.Done(): ... }

对 I/O 操作通常用接 ctx 的版本:

req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)

row := db.QueryRowContext(ctx, "SELECT ...", args...)

4. 链式派生

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

// 派生:更短超时
ctxShort, cancel2 := context.WithTimeout(ctx, 2*time.Second)
defer cancel2()

callRemote(ctxShort)

子 ctx 永远比父 ctx 早结束。取较短的 timeout 生效。

父 cancel → 所有子 ctx 都被 cancel。

5. ctx 传值

type traceIDKey struct{}

ctx = context.WithValue(ctx, traceIDKey{}, "abc-123")

// 下游取
tid, ok := ctx.Value(traceIDKey{}).(string)

约定:

  • key 用未导出的私有 type,避免不同包冲突
  • 只传"跨 API 边界的请求作用域元数据"(trace ID / user ID / locale),
    不传业务参数
  • 业务参数走显式函数签名

6. context.Background() vs context.TODO()

  • Background():根 context,main / init 用
  • TODO():当你不知道用哪个 ctx 时用(让 linter / 自己后续修)
ctx := context.TODO()   // I'll come back to wire this

7. HTTP 服务端:取请求 ctx

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()   // 客户端断开连接 → ctx.Done()

    result, err := fetchFromDB(ctx, ...)
    if err == context.Canceled {
        // 客户端已经断开,没必要返回响应
        return
    }
    json.NewEncoder(w).Encode(result)
}

客户端 close 连接时 r.Context 自动 cancel,所有下游 query 应自动中断。
省服务器 CPU / DB 连接。

8. errgroup + ctx 并发

sync/errgroup 比手写 channel + WaitGroup 简洁:

import "golang.org/x/sync/errgroup"

func fetchAll(ctx context.Context, urls []string) ([]string, error) {
    g, ctx := errgroup.WithContext(ctx)
    results := make([]string, len(urls))

    for i, u := range urls {
        i, u := i, u   // capture
        g.Go(func() error {
            data, err := fetch(ctx, u)
            if err != nil {
                return err   // 任一失败 ctx 自动 cancel 其它
            }
            results[i] = data
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err
    }
    return results, nil
}

errgroup.WithContext 返回的 ctx 在任一 goroutine 返回 error 时
自动 cancel。

限并发:

g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)   // 最多 10 个 goroutine 并发

9. 不要把 ctx 存结构体

反模式:

type Server struct {
    ctx context.Context   // ❌
}

ctx 是请求 / 操作的属性,不是对象的属性。每次方法显式传:

type Server struct{}

func (s *Server) Handle(ctx context.Context, req Req) (Resp, error) { ... }

例外:长生命周期的"管理者"对象(如 server 自身的 shutdown ctx)
可以存——但要清楚标注。

10. cancel() 必须 defer

ctx, cancel := context.WithTimeout(..., 5*time.Second)
// 忘 defer cancel() → 即使函数正常返回,定时器还在 → goroutine leak
defer cancel()

go vet 会警告"the cancel function is not used on all paths",
认真处理。

11. ctx 的零成本约定

约定 ctx 永远作为第一个参数:

// ✅
func doWork(ctx context.Context, args Args) (Result, error)

// ❌
func doWork(args Args, ctx context.Context) (Result, error)

阅读 / IDE 补全 / linter 都基于这个约定。

12. 实际生产 pattern

func main() {
    ctx, cancel := signal.NotifyContext(context.Background(),
        syscall.SIGINT, syscall.SIGTERM)
    defer cancel()

    srv := &http.Server{Addr: ":8080", Handler: setupHandler()}

    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("server error: %v", err)
        }
    }()

    <-ctx.Done()
    log.Info("shutting down")

    shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Errorf("forced shutdown: %v", err)
    }
}

signal.NotifyContext(Go 1.16+)一行处理信号 → ctx 模式:
- 收 SIGINT / SIGTERM → ctx.Done()
- main 等 done
- srv.Shutdown(timeout ctx) 拒新连接 + 等老请求完,30 秒超时强制退出

踩过的坑

  • ctx 没传到下游:所有 hop 都要传 ctx。中间某个函数没接 → cancel 信号
    断了,超时不生效。
  • time.Sleep(d) 而不是 select { case <-time.After: case <-ctx.Done():}
    cancel 后还在 sleep,不响应。
  • errgroup.Go 里的 goroutine panic → 整个 group 崩。在 Go 函数内部
    recover 自保。
  • ctx.Value(string) 用字符串作 key → 跨包冲突。永远用私有 type。
精确评价 共 0 人评价
可复现性
可复现 · 0 不可复现 · 0
文风
文风流畅 · 0 文风晦涩 · 0
立场
支持 · 0 反对 · 0

登录后即可对本帖作出评价。

评论区 0 条 · 所有人可在此交流

登录后参与评论。

还没有评论,来说两句。