★ context 的树形结构、取消与超时是如何工作的?传值有什么纪律?
结论先行
context 用“不可变节点 + 从父派生”的方式组织成树:WithCancel/WithDeadline/WithTimeout/WithValue 都从父 ctx 派生出子 ctx;父被取消或超时到点时,会级联取消整棵子树(通过关闭 Done channel 广播)。它承担两类职责:传递取消信号与截止时间、携带少量请求级元数据,使用上有明确纪律。
要点
- 派生与级联:子 ctx 持有父的引用并监听父的 Done;父 cancel 会同步取消所有子孙,形成“取消树”。
- 取消机制:ctx.Done() 返回的 channel 在取消时被 close(只关一次);监听方 select 到即停止工作;cancel 函数幂等,可重复调用。
- 超时与截止:WithTimeout 底层是 WithDeadline 加定时器,到点自动 cancel;适合给 RPC、DB 调用设硬性时限。
- 标准姿势:ctx 作为函数第一个参数贯穿调用链;服务端从请求入口获得根 ctx(如 http.Request.Context())。
- goroutine 义务:凡会阻塞或长跑的 goroutine 都要监听 ctx.Done(),被取消时及时退出、释放资源,避免泄漏。
- 传值纪律:只传“请求级、跨 API 边界”的信息(traceID、用户身份),不传可选业务参数;Value 查找沿树向上,滥用影响性能与耦合。
- key 规范:用自定义的私有类型做 key(避免 string 撞车);存入的值应只读、并发安全。
示例
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // 释放定时器资源
err := fetch(ctx)
fmt.Println(err) // context deadline exceeded
func fetch(ctx context.Context) error {
select {
case <-time.After(3 * time.Second): // 模拟慢调用
return nil
case <-ctx.Done(): // 超时被取消
return ctx.Err()
}
}
常见追问/记忆点
- 追问:WithTimeout 后忘了 cancel 会怎样?答:定时器与相关资源延迟释放,高频创建会累积开销,defer cancel() 要养成习惯。
- 追问:context 适合存业务参数吗?答:不适合,应显式传参;context 只做请求级元数据的隐式通道。
- 追问:子 context 取消会影响父吗?答:不会,取消只向下传播,父取消子,子不能取消父。
- 记忆:父灭子灭(级联取消)、Done 广播、超时等于定时取消;cancel 要 defer,value 要克制。