Context 是 Go 并发编程的核心。没有它,你的 goroutine 就像脱缰的野马——跑起来不知道怎么停,超时了也没法杀。这篇彻底搞懂 context。
Q1:Context 到底解决什么问题?
A: 一个 goroutine 启动后,调用方面临三个问题:
| 问题 |
没有 Context |
有 Context |
| 取消任务 |
用户关了页面,后台 goroutine 还在跑 |
ctx.Cancel() 一键停掉 |
| 超时控制 |
数据库卡住了?请求永远不返回 |
ctx.Timeout() 自动取消 |
| 传值 |
goroutine 需要请求 ID、用户信息 |
ctx.Value() 跨层传递 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| func fetchData() ([]User, error) { ch := make(chan []User) go func() { data := queryFromDB() ch <- data }() return <-ch, nil }
func fetchData(ctx context.Context) ([]User, error) { ch := make(chan []User, 1) go func() { data := queryFromDB(ctx) ch <- data }() select { case data := <-ch: return data, nil case <-ctx.Done(): return nil, ctx.Err() } }
|
Q2:四种 Context 怎么选?
A:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ctx := context.Background() ctx := context.TODO()
ctx, cancel := context.WithCancel(parentCtx) defer cancel() go doSomething(ctx) cancel()
ctx, cancel := context.WithTimeout(parentCtx, 3*time.Second) defer cancel()
deadline := time.Now().Add(5 * time.Minute) ctx, cancel := context.WithDeadline(parentCtx, deadline) defer cancel()
ctx = context.WithValue(parentCtx, "requestID", "abc-123")
|
选择决策树:
1 2 3 4 5 6
| 需要什么? ├── 只是一个根节点 → context.Background() ├── 需要手动取消 → WithCancel ├── 需要超时限制 → WithTimeout(最常用) ├── 需要在特定时间点截止 → WithDeadline └── 需要跨函数传值 → WithValue(谨慎使用)
|
Q3:WithTimeout 实际项目中怎么用?给个完整示例
A: HTTP 服务中的典型模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| func (s *Server) GetUser(w http.ResponseWriter, r *http.Request) { ctx := r.Context()
dbCtx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel()
user, err := s.repo.GetUser(dbCtx, userID) if err != nil { if errors.Is(err, context.DeadlineExceeded) { http.Error(w, "DB query timeout", http.StatusGatewayTimeout) return } if errors.Is(err, context.Canceled) { log.Printf("client disconnected: %v", ctx.Err()) return } }
json.NewEncoder(w).Encode(user) }
|
关键细节:
r.Context() 在客户端断开连接时会自动 cancel
WithTimeout 创建子 context,超时不影响父 context
defer cancel() 必须写! 不写会导致资源泄漏
Q4:WithValue 该不该用?听说社区有争议?
A: 确实有争议。先用代码看清楚:
正确用法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| type contextKey string
const ( UserIDKey contextKey = "userID" RequestIDKey contextKey = "requestID" )
func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") userID := validateToken(token) ctx := context.WithValue(r.Context(), UserIDKey, userID) next.ServeHTTP(w, r.WithContext(ctx)) }) }
func Handler(w http.ResponseWriter, r *http.Request) { userID := r.Context().Value(UserIDKey).(string) }
|
为什么要有争议?
| 问题 |
说明 |
| 类型不安全 |
Value() 返回 interface{},需要类型断言 |
| 隐式依赖 |
函数签名看不出需要什么值 |
| 容易滥用 |
有人把什么都塞进 context |
| 测试麻烦 |
要手动构造带值的 context |
最佳实践:
- 定义私有 key 类型(
contextKey),避免冲突
- 只放请求范围的数据(requestID、userID、traceID、language)
- 不要放可选参数——那应该用函数参数或选项模式
- 不要放指针——context 取出的值应该是不可变的
Q5:Context 传播规则是什么?
A: 记住一张图:
1 2 3 4 5 6
| Background (根) ├── WithCancel(A) ──── A 被取消 → A 和所有子节点都取消 │ ├── WithTimeout(B) ─── B 超时 → B 和它的子节点被取消,A 不受影响 │ │ └── WithValue(C) ─── C 是纯值传递,不能取消 │ └── WithCancel(D) └── WithCancel(E) ──── E 和 A 是兄弟,互不影响
|
核心规则:
- 父取消 → 所有子节点自动取消
- 子取消 → 不影响父和兄弟
- 一旦 cancel 就不可恢复(context 是单向的)
- WithValue 创建的 context 不能 cancel
Q6:常见错误有哪些?
A:
错误 1:忘记 defer cancel()
1 2 3 4 5 6 7 8 9 10 11 12
| func bad() { ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) doSomething(ctx) }
func good() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() doSomething(ctx) }
|
即使超时了也要 cancel,因为内部资源可能需要立即释放。
错误 2:把 context 放进结构体
1 2 3 4 5 6 7
| type Service struct { ctx context.Context }
func (s *Service) DoSomething(ctx context.Context) error { ... }
|
context 应该在调用链中流动,不应该存储在结构体里。
错误 3:用 context 传函数选项
1 2 3 4 5 6
| ctx = context.WithValue(ctx, "limit", 100) ctx = context.WithValue(ctx, "offset", 0)
func Query(ctx context.Context, opts ...QueryOption) (*Result, error)
|
Q7:标准库中哪些地方用了 Context?
A: 几乎所有涉及 I/O 的标准库都支持:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| http.Server.Shutdown(ctx)
db.ExecContext(ctx, "INSERT INTO...") db.QueryRowContext(ctx, "SELECT...")
http.NewRequestWithContext(ctx, "GET", url, body)
client.Call(ctx, req) server.RegisterHandler(func(ctx, req) {})
cmd.Start() + cmd.Wait() → exec.CommandContext(ctx, "ls", "-la")
|
如果你在写的函数可能耗时或阻塞,加一个 context.Context 参数是 Go 社区的约定。
下期预告
下一篇聊 Testing:表驱动测试 + Benchmark。Go 的测试哲学和别的语言很不一样——没有复杂的框架,但 testing 包配合 table-driven test 能写出非常优雅的测试代码。
TODO: 回顾你 slg-go 项目里的 handler 或 service 方法——每个涉及网络/数据库调用的方法都有 context.Context 作为第一个参数吗?如果没有,现在就是重构的好时机。