Files
cs-note/hzh/GolangStar/Go语言原理/context原理.md
T

276 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<br/>Deadline/Done/Err/Value"] --> empty["emptyCtx<br/>根 context"]
C --> cancel["cancelCtx<br/>可取消"]
C --> timer["timerCtx<br/>可取消 + 定时"]
C --> value["valueCtx<br/>键值对传递"]
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<br/>key2=val2"] --> V1["valueCtx1<br/>key1=val1"]
V1 --> E["emptyCtx<br/>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 相关高频面试题