context 的价值,不在于“多传一个参数”,而在于把请求的生命周期显式地传到函数边界上。

为什么要有 context

在 Web 服务里,一个请求往往会串起数据库、缓存、RPC 和下游 HTTP 调用。如果上游请求已经取消,或者超时已经发生,后续操作继续执行通常只是在浪费资源。

这个时候,context.Context 可以把“这个请求还是否有效”一路传下去。

最常用的两个能力

1. 超时控制

1ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
2defer cancel()
3
4err := service.FetchProfile(ctx, userID)
5if err != nil {
6    return err
7}

2. 取消传播

上层 cancel() 之后,下游持有同一条上下文链的操作会收到 Done() 信号。

1select {
2case <-ctx.Done():
3    return ctx.Err()
4case result := <-ch:
5    return result.err
6}

一些容易犯的错

不要把 context 存进 struct

推荐把它作为每个函数的第一个参数传进去,而不是在对象创建时缓存起来,否则生命周期会变得模糊。

不要传 nil

如果当前实在没有可用上下文,也应该传 context.Background()context.TODO()

不要滥用 Value

context.WithValue 更适合放 trace id、request id 这种链路信息,不适合存大量业务字段。

一个简单的调用链示例

 1func Handler(w http.ResponseWriter, r *http.Request) {
 2    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
 3    defer cancel()
 4
 5    if err := QueryUser(ctx, 42); err != nil {
 6        http.Error(w, err.Error(), http.StatusGatewayTimeout)
 7        return
 8    }
 9}
10
11func QueryUser(ctx context.Context, id int64) error {
12    req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
13    if err != nil {
14        return err
15    }
16
17    _, err = http.DefaultClient.Do(req)
18    return err
19}

我的总结

  • context 是生命周期控制工具,不是参数收纳盒
  • 超时和取消要从入口尽早设置
  • 下层库函数如果支持 context,优先传带超时的版本