context.Background()返回emptyCtx作为根节点,实际功能由cancelCtx和timerCtx实现,其done通道惰性创建、children映射支持级联取消、timer触发自动终止;Value()线性向上查找,需防误匹配与panic;propagateCancel和removeChild决定引用是否泄漏。

直接看 context.go 文件,别从文档或博客起步——90% 的困惑来自跳过源码结构直接读解释。
从 context.Background() 入口开始追,不是从接口定义
很多人一上来就盯着 type Context interface 看,结果卡在“这接口谁实现的?”,其实该先看最常用的起点:context.Background()。它返回一个 background 全局变量,类型是 emptyCtx —— 这是个空实现,不带任何逻辑,只用于做根 context。它的存在意义就一个:避免传 nil。
你真正要关注的是它怎么被“派生”出去的,比如:WithCancel()、WithValue() 这些函数内部如何构造新结构体并挂载到父节点上。
重点盯住 cancelCtx 和 timerCtx 的字段与 cancel() 方法
这两个结构体是实际干活的主力,它们都嵌入了 Context 接口,并额外携带状态:
-
cancelCtx.done是惰性创建的chan struct{},首次调用Done()才初始化; -
cancelCtx.children是个map[canceler]struct{},用来级联取消——子 cancelCtx 被 cancel 时,会遍历这个 map 把所有孩子也 cancel 掉; -
timerCtx.timer是个*time.Timer,WithDeadline和WithTimeout都靠它触发自动 cancel; -
cancel()方法里有关键逻辑:关闭done、清空children、停止timer(如果存在),缺一不可,否则泄漏。
Value() 查找是线性遍历,不是哈希查找
WithValue() 创建的 valueCtx 是链表式嵌套结构,每次调用 ctx.Value(key) 都是从当前节点开始,顺着 Context 字段一路向上查,直到 emptyCtx。这意味着:
立即学习“go语言免费学习笔记(深入)”;
- key 类型最好用导出的常量(如
type keyType string+const RequestIDKey keyType = "req_id"),避免字符串字面量误匹配; - 不要在热路径高频调用
Value(),尤其是嵌套很深的 context 树; -
Value()返回interface{},强制类型断言失败时 panic,务必加ok判断,例如:v, ok := ctx.Value(myKey).(string); - 同一个 key 多次
WithValue(),只会取最靠近叶子节点的那个值,上层的被遮蔽。
别忽略 propagateCancel() 和 removeChild() 的时机问题
这是最容易引发 goroutine 泄漏的地方。当你调用 WithCancel(parent),它不仅新建 cancelCtx,还会调用 propagateCancel(parent, &c) 尝试把子节点注册到父节点的 children map 中。但这个注册不是总成功:
- 如果
parent已经被 cancel,就不会注册,子节点变成“孤儿”,后续你调用cancel()只关自己,不影响父节点; - 如果父节点是
valueCtx或emptyCtx,它们不实现canceler接口,注册也会失败; - 子节点 cancel 后,必须调用
removeChild()从父节点 map 中清理自己,否则父节点一直 hold 引用,导致整个子树无法 GC; -
go vet会检查CancelFunc是否在所有分支都被调用,但不会检查是否漏掉removeChild—— 这得靠你读源码时留意。
真正难的不是理解 context 做什么,而是搞清它在哪一刻建立/切断父子引用、channel 何时被创建/关闭、timer 何时启动/停止——这些细节全藏在那几百行 context.go 里,不在注释里,在代码执行路径上。


















