Context继承通过父子引用构树而非复制,WithCancel/Timeout/Deadline/Value均创建新节点挂载父下;取消时递归通知子节点,emptyCtx不可变,误用会导致覆盖、泄漏或无效操作。

context 的继承不是“复制”或“克隆”,而是通过父子引用构建一棵树。每个子 Context 都持有一个指向父 Context 的指针,取消、超时、值查询等行为都沿着这条引用链向上回溯。
context.WithCancel / WithTimeout / WithDeadline / WithValue 都是派生操作
这四个函数接收一个 parent 参数,返回新 Context 和(可选)控制函数。它们不修改原 Context,而是创建新节点并挂到父节点下:
-
WithCancel创建cancelCtx节点,内部维护children map[canceler]struct{},取消时遍历子节点递归调用cancel() -
WithTimeout实际是先调用WithDeadline,把time.Now().Add(timeout)作为截止时间传入 -
WithValue创建valueCtx节点,只保存key和val,查找Value(key)时逐层向上匹配,直到根或找到第一个匹配项 - 多次调用
WithValue不会覆盖——后派生的节点在链上更靠近叶子,Value()查找时优先返回离当前节点最近的那个key值
取消信号如何沿树传播
调用任意节点的 cancel() 函数,会触发三件事:
- 关闭该节点自己的
donechannel - 设置
err字段为context.Canceled - 遍历
children映射,对每个子cancelCtx节点也调用其cancel()
注意:valueCtx 和 timerCtx 没有自己的 children 字段,但它们的底层嵌套结构仍保留父引用;一旦父节点 done 关闭,所有子孙的 Done() 返回的 channel 都会立即可读。
为什么不能用 context.Background() 直接赋值再修改
context.Background() 返回的是 emptyCtx 类型,它:
立即学习“go语言免费学习笔记(深入)”;
- 没有
donechannel,Done()永远返回nil -
Deadline()和Err()总是返回零值 -
Value()对任何key都返回nil - 它本身不可取消、不可设超时、不可存值——所有功能必须靠派生子节点提供
所以你不能写 ctx := context.Background(); ctx = context.WithCancel(ctx) 然后期望原 ctx 变成可取消的。它只是新建了一个节点,原变量仍是 emptyCtx。
实际编码中容易忽略的层级陷阱
常见误用包括:
- 把
WithValue当全局配置用,在多个不相关的 goroutine 中混用同一个 key,导致值被意外覆盖或读错层级 - 在 HTTP handler 中用
req.Context()派生子 context 后,又把它塞回http.Request(Go 标准库不允许修改req.ctx) - 忘记调用
cancel(),导致cancelCtx节点及其整个子树无法被 GC,形成内存泄漏(尤其在长连接或定时任务中) - 在
select中监听ctx.Done()但没检查ctx.Err(),无法区分是主动取消还是超时,影响错误分类和日志
树的深度本身没有硬限制,但深层嵌套会增加 Value() 查找开销;真正关键的是逻辑归属——每个 Context 应只由创建它的那一层负责生命周期管理,跨层传递时别擅自 cancel 或重绑定 value。


















