context.WithCancel能级联取消所有子context,因其cancelCtx内部维护children map,调用cancel时先关闭done通道,再递归调用各子节点cancel方法,并置err为context.Canceled。

context.WithCancel 为什么能级联取消所有子 context
因为 cancelCtx 内部维护了一个 children map,每次调用 WithCancel、WithTimeout 或 WithDeadline 创建子 context 时,父节点都会把子节点注册进这个 map。一旦调用 cancel 函数,它会:
- 关闭自己的
donechannel(触发所有监听Done()的 goroutine) - 递归遍历并调用每个子
cancelCtx的 cancel 方法(实现级联) - 清空
children并置err字段为context.Canceled
注意:WithValue 创建的 valueCtx 不参与取消传播——它只是透传父节点的 Done() 和 Err(),自身没有取消逻辑。
ctx.Value(key) 取不到值的常见原因
不是 key 类型不匹配就是链路断了。Go 的 Value() 是纯链式查找:从当前 context 开始,逐层向上调用 parent.Value(key),直到遇到 emptyCtx(如 context.Background())返回 nil。
- 用字符串字面量当 key:
ctx = context.WithValue(ctx, "user_id", u)→ 下游写ctx.Value("user_id")看似对,但 Go 中不同包里的相同字符串字面量是不同地址,比较结果为 false - 正确做法是定义私有类型:
type userKey struct{},然后统一用userKey{}实例作 key - 函数没把 ctx 传下去:比如中间某层忘了把入参
ctx传给下一层调用,链路就断了
WithTimeout 和 WithDeadline 的底层差异在哪
WithTimeout 是相对时间,WithDeadline 是绝对时间,但它们底层都用 timerCtx 实现,区别只在初始化时怎么设 deadline 字段。
-
WithTimeout(parent, d)→deadline = time.Now().Add(d) -
WithDeadline(parent, t)→ 直接赋值deadline = t - 两者都会启动一个
*time.Timer,到点后自动调用内部 cancel 函数 - 如果提前手动调用 cancel,timer 会被
Stop(),避免泄漏
别混用:用 WithTimeout 就别再算时间加到 Now() 上,容易因系统时钟跳变或 NTP 同步导致 deadline 错乱。
为什么不能把 context 存进 struct 里长期持有
因为 context 是请求生命周期的载体,不是全局状态。存进 struct 意味着:它可能被多个 goroutine 并发读写、超时时间固定不变、取消信号无法及时响应新请求。
- 违反 “context 应作为第一个参数显式传递” 原则,测试和 mock 困难
- 如果 struct 生命周期长于单次请求(比如 HTTP handler 里缓存了某个 ctx),后续请求复用它会导致超时/取消逻辑错乱
- 更隐蔽的问题:
WithValue数据会累积污染,子 context 的Value()查找路径变长,性能下降
真正该进 struct 的,是那些不随请求变化的依赖项,比如数据库连接池、配置对象——而 context 必须每次新请求都重新构造。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











