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,优先传带超时的版本