context.WithCancel 通过 cancelCtx 结构体建立父子关系:嵌入父 Context、维护加锁的 children map,父取消时遍历调用子 cancel;Done 关闭仅表结束,Err() 需等待关闭后才返回具体错误。

Go 的 context.Context 不是黑箱,它的传递和取消机制完全基于接口契约 + 显式派生 + 同步通知,没有魔法,但容易因误用导致信号丢失或 goroutine 泄漏。
context.WithCancel 是如何建立父子关系的
调用 context.WithCancel(parent) 会返回一个新 Context 和一个 cancel 函数。这个新 Context 内部是一个 cancelCtx 结构体,它通过嵌入父 Context 并维护一个 children map[canceler]struct{} 来记录所有由它派生出的子 context。
关键点在于:父 context 取消时,会遍历 children map,对每个子节点调用其 cancel 方法 —— 这是递归传播的基础。
- 子 context 必须由父 context 派生(如
WithCancel/WithTimeout),不能手动构造或跨链赋值 -
childrenmap 是加锁访问的(mu sync.Mutex),保证并发安全,但锁粒度在 cancelCtx 实例上,不是全局 - 如果子 context 已被取消过,再次调用其
cancel是幂等的,不会 panic,但也不会重新触发传播
Done channel 关闭后为什么 Err() 才返回非 nil
ctx.Done() 返回的是一个只读 chan struct{},关闭即表示“该 context 已结束”。但 ctx.Err() 必须等到 channel 关闭后才返回具体错误(如 context.Canceled 或 context.DeadlineExceeded)。
立即学习“go语言免费学习笔记(深入)”;
这是因为 Err() 的语义是“为什么结束”,而 channel 关闭只是“已结束”的信号,原因需要在 cancel 逻辑中显式设置并同步写入。
- 直接读
只能知道“结束了”,无法区分是超时还是主动取消 - 正确做法是 select 等待
ctx.Done(),然后立刻调用ctx.Err()判断原因 - 不要在
Done()关闭前反复调用Err()—— 它会一直返回nil,直到内部状态更新完成
WithValue 为什么不能替代函数参数
context.WithValue(parent, key, val) 把键值对存进 context 树,后续可通过 ctx.Value(key) 获取。但它设计初衷仅用于传递请求范围的元数据(如 traceID、userID、auth token),而非业务逻辑参数。
根本原因是 Value() 是 interface{} 类型,无编译期类型检查,且 key 冲突风险高(比如两个包都用 string 当 key)。
- key 建议定义为未导出的私有 struct 类型(如
type userKey struct{}),避免冲突 - 不要把数据库连接、配置对象、回调函数等传进
WithValue—— 它们应该作为函数参数显式传递 -
WithValue是浅拷贝,value 本身不参与 context 生命周期管理;value 若含指针或 map,需自行保证线程安全
为什么 Background 和 TODO 看似一样却不能混用
context.Background() 和 context.TODO() 都返回 emptyCtx 实例,底层行为一致:永不取消、无 deadline、无 value。区别纯属语义约定。
它们的唯一作用是提供一个“根” context,让后续 WithXXX 调用有起点。但选错会影响代码可维护性。
- 启动服务、HTTP handler 入口、main 函数中应使用
context.Background() - 函数签名已定、暂时没想好传哪个 context,或第三方库尚未适配 context 时,用
context.TODO()—— 它是明确的“待办标记” - 永远不要在生产代码里把
TODO()留着不改;也别在测试中用Background()模拟可取消场景(应改用WithCancel)
真正难的不是理解 cancel 怎么传播,而是判断哪些 goroutine 应该监听同一个 ctx.Done()、哪些该用独立 timeout、哪些压根不该接入 context —— 这些边界问题没有银弹,得靠对业务生命周期的把握。


















