--- tags: [go, golang, go-principle, context] create time: 2026-06-07 15:30 --- # Context 底层原理 ## 概述 本文从源码角度解析 Go `context` 包的四种实现(emptyCtx / cancelCtx / timerCtx / valueCtx),以及取消传播链和值传递链的底层机制。Context 是 Go 并发编程中控制取消和数据共享的核心工具,理解其实现能让你写出更健壮的并发程序。 > [!question] ❓ 思考 > 为什么 `WithValue` 派生的 context 层层嵌套形成链表,而不是用 map 存储?当父 context 被取消时,子 context 是如何级联收到信号的? ## 正文 ### 一、Context 的类型体系 ```mermaid graph TB C["Context interface
Deadline/Done/Err/Value"] --> empty["emptyCtx
根 context"] C --> cancel["cancelCtx
可取消"] C --> timer["timerCtx
可取消 + 定时"] C --> value["valueCtx
键值对传递"] timer -.嵌入.-> cancel cancel -.嵌入.-> C value -.嵌入.-> C B["Background()"] --> empty T["TODO()"] --> empty WC["WithCancel(parent)"] --> cancel WD["WithDeadline/Timeout(parent)"] --> timer WV["WithValue(parent, key, val)"] --> value style C fill:#e3f2fd style empty fill:#e8f5e9 style cancel fill:#fff9c4 style timer fill:#fff3e0 style value fill:#fce4ec ``` 四个结构体的核心职责: | 类型 | 可取消 | 有 Deadline | 存值 | 用途 | |------|--------|-------------|------|------| | `emptyCtx` | 否 | 否 | 否 | 根 context (Background/TODO) | | `cancelCtx` | 是 | 否 | 否 | WithCancel 派生 | | `timerCtx` | 是 | 是 | 否 | WithDeadline/Timeout 派生 | | `valueCtx` | 否 | 否 | 是 | WithValue 派生 | ### 二、取消传播链:cancelCtx #### 数据结构 ```go type cancelCtx struct { Context // 嵌入父 context mu sync.Mutex // 保护以下字段 done atomic.Value // chan struct{},nil 或未关闭 → 未取消;已关闭 → 已取消 children map[canceler]struct{} // 子 canceler 集合 err error // 取消原因 } ``` #### Done 通道的懒汉创建 ```go func (c *cancelCtx) Done() <-chan struct{} { d := c.done.Load() if d != nil { return d.(chan struct{}) } c.mu.Lock() defer c.mu.Unlock() d = c.done.Load() // 双重检查 if d == nil { d = make(chan struct{}) c.done.Store(d) } return d.(chan struct{}) } ``` 注意这个 channel 是**只读的**——只有父 context 关闭它,子 goroutine 通过 `select` 监听它来感知取消信号。 #### 取消逻辑:递归级联 ```go func (c *cancelCtx) cancel(removeFromParent bool, err error) { c.mu.Lock() if c.err != nil { c.mu.Unlock(); return } // 已取消 c.err = err d, _ := c.done.Load().(chan struct{}) if d == nil { c.done.Store(closedchan) // 预创建的 closed chan } else { close(d) // 关闭通道,通知所有监听者 } for child := range c.children { child.cancel(false, err) // 递归取消子节点 } c.children = nil c.mu.Unlock() if removeFromParent { removeChild(c.Context, c) // 从父节点移除自己 } } ``` 取消流程: ```mermaid flowchart TD P["父 cancelCtx"] --> C1["子 cancelCtx 1"] P --> C2["子 cancelCtx 2"] C1 --> C1a["孙 cancelCtx"] C2 --> C2a["孙 valueCtx"] style P fill:#ffebee style C1 fill:#fff3e0 style C2 fill:#fff3e0 style C1a fill:#e8f5e9 style C2a fill:#e8f5e9 click P "触发 cancel()" click C1 "级联取消" click C2 "级联取消" ``` 调用 `cancel()` 后: 1. 关闭自己的 `done` channel → 所有监听该 channel 的 goroutine 收到信号 2. 递归取消所有子节点 3. 将自己从父节点的 children 中移除 ### 三、父子关联:propagateCancel ```go func propagateCancel(parent Context, child canceler) { done := parent.Done() if done == nil { return // 父节点永远不会被取消 } select { case <-done: child.cancel(false, parent.Err()) // 父已取消,子直接取消 return default: } // 尝试从父提取 cancelCtx if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { child.cancel(false, p.err) } else { if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { // 父不是标准 cancelCtx,启动 goroutine 监控 atomic.AddInt32(&goroutines, +1) go func() { select { case <-parent.Done(): child.cancel(false, parent.Err()) case <-child.Done(): } }() } } ``` 三种情形: | 父 context 情况 | 处理方式 | |----------------|---------| | `Done() == nil`(永远不取消) | 无需关联 | | 能提取出 `cancelCtx` | 直接加入 children map | | 不能提取 `cancelCtx`(如 valueCtx 链中的某层) | 起一个 goroutine 监控 | ### 四、值传递链:valueCtx #### 数据结构 ```go type valueCtx struct { Context // 父 context key, val interface{} // 当前层的键值对 } ``` 每个 `valueCtx` 只存**一对**键值对,多层 `WithValue` 会形成嵌套链表: ```mermaid flowchart LR V2["valueCtx2
key2=val2"] --> V1["valueCtx1
key1=val1"] V1 --> E["emptyCtx
Background"] style V2 fill:#fce4ec style V1 fill:#fff3e0 style E fill:#e8f5e9 ``` 查找过程: ```go func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) // 向上递归查找 } ``` > [!warning] ⚠️ 性能提示 > Value 查找是 O(n) 线性搜索(n = 嵌套层数)。过深的嵌套会影响性能,建议控制在合理范围内。同时,key 应避免使用容易碰撞的类型,推荐使用自定义不可比较类型。 ```go // 推荐的 key 定义方式 type ctxKey string const myKey ctxKey = "my-key" ctx := context.WithValue(ctx, myKey, value) ``` ### 五、TimerCtx:定时取消 ```go type timerCtx struct { cancelCtx timer *time.Timer deadline time.Time } ``` `timerCtx` 嵌入了 `cancelCtx`,额外增加了一个定时器。在 `WithDeadline` 中: ```go func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { if cur, ok := parent.Deadline(); ok && cur.Before(d) { return WithCancel(parent) // 父 deadline 更早,直接返回 cancelCtx } c := &timerCtx{cancelCtx: newCancelCtx(parent), deadline: d} propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded) return c, func() { c.cancel(false, Canceled) } } c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) return c, func() { c.cancel(true, Canceled) } } ``` ### 六、最佳实践 > [!tip] 💡 技巧 > - **永远将 context 作为第一个参数**传入函数,命名为 `ctx` > - **不要将 context 存入结构体**,只作为请求级别的传递工具 > - **不要用 context 传值做业务数据交换**,它是跨 API 边界的取消信号机制 > - **长生命周期对象不要用 context 传值**,会导致对象无法被 GC ## 小结 - Context 是接口,四种实现各司其职:空、取消、定时、传值 - 取消通过 `close(done channel)` + 递归遍历 children map 实现级联传播 - 值传递通过嵌套链表实现,查找为 O(n) 线性搜索 - `propagateCancel` 处理父子关联,支持三种场景 ## 关联笔记 - [[hzh/GolangStar/Go语言进阶/Context]] — Context 的基础用法 - [[hzh/GolangStar/Go语言进阶/协程池]] — Context 在 worker pool 中的应用 - [[hzh/GolangStar/Go面试题库/Context面试题]] — Context 相关高频面试题