--- tags: [go/lang, context, cancellation, deadline-propagation, goroutine-lifecycle] create time: 2026-08-08 19:00 update time: 2026-08-08 19:00 --- # Context 包详解 ## 概述 Context 是 Go 1.7 引入的标准库,用于在 goroutine 树中传递上下文信息、取消信号和截止时间。它不是数据传递的载体(那是结构体的职责),而是控制并发生命周期的信号总线。理解 ctx 的链式传播机制和 WithValue 的性能陷阱,对编写健壮的并发程序至关重要。 > [!NOTE] 一句话定义 > Context 是一个接口,核心方法只有 Done() 和 Err()——它不提供数据传输能力,只提供"告诉下游我该停了"的信号通道。 ## 核心原理 ### Context 接口定义 ```go type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any } ``` 四个方法各有分工: - **Deadline**:返回取消时间。未设置 deadline 的 ctx 返回 `ok=false` - **Done**:返回一个只读 channel。ctx 被取消时该 channel 会关闭 - **Err**:返回取消原因(Canceled / DeadlineExceeded) - **Value**:键值对查询(不推荐用于业务传参) > [!WARNING] 常见认知错误 > Context 不是用来在 goroutine 间传递业务参数的替代品。如果多个 goroutine 需要共享配置或请求数据,应该用结构体或参数列表。WithValue 性能很差且容易引发问题(见后文)。 ### 四种 WithXXX 函数源码级分析 #### WithCancel — 手动取消 ```go func WithCancel(parent Context) (ctx Context, cancel CancelFunc) ``` 实现本质非常简单——创建一个 `cancelCtx` 节点,挂载到父 ctx 上。当 `cancel()` 被调用时: 1. 设置 `err = Canceled` 2. 关闭 `done` channel(所有监听者收到关闭信号) 3. 递归通知子 ctx 也取消 ```mermaid flowchart LR A["parent ctx"] -->|"ctx1, _ := WithCancel"| B["ctx1\n(cancelCtx)"] B -->|"ctx2, _ := WithCancel ctx1"| C["ctx2\n(sub-cancelCtx)"] C -->|"ctx3, _ := WithCancel ctx2"| D["ctx3\n(sub-sub-cancelCtx)"] style B fill:#e3f2fd style C fill:#e8f5e9 style D fill:#fce4ec ``` 调用 `cancel()` 时信号沿虚线向上传播到所有后代。 #### WithTimeout — 超时取消 ```go func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) ``` WithTimeout 内部组合了 WithCancel 和一个 timer: ```go func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { return WithDeadline(parent, time.Now().Add(timeout)) } ``` 底层启动一个 goroutine 等待 timer 触发后自动调用 cancel。这意味着 WithTimeout 本身比 WithCancel 多了一个 goroutine 开销。 #### WithDeadline — 绝对截止时刻 ```go func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) ``` 与 WithTimeout 的区别仅在于指定的是绝对时间而非相对时长。两者底层使用同一个 `timerCtx` 结构体,包含 `*cancelCtx` 和 `time.Timer`。 > [!TIP] 面试考点 > WithTimeout 本质上就是 WithDeadline(now + timeout)。面试官如果追问"两者的区别是什么",这就是标准答案。 #### WithValue — 附带值传递 ```go func WithValue(parent Context, key, val any) Context ``` WithValue 不创建新的类型,只是在 context 树上追加一层带有 key-value 对的节点。每次调用都会产生一个新的 context 对象。 ### 链式传播机制 Context 通过嵌套构成一棵树。每个 `cancelCtx` 维护一个 `children` 列表来追踪直接子节点: ```go type cancelCtx struct { Context mu sync.Mutex done chan struct{} children map[canceler]*cancelCtx // 子节点集合 err error // 取消时的错误原因 childCleared bool // 是否已清理子节点 } ``` 当父 ctx 被取消时,遍历 `children` 逐个取消子节点。这个传播过程是递归的: ``` parent.WithCancel → ctx1 ctx1.WithCancel → ctx2 ctx2.WithCancel → ctx3 cancel() at parent │ ├── 递归取消 ctx1 ──→ 递归取消 ctx2 ──→ 递归取消 ctx3 ``` > [!NOTE] 为什么不用双向指针? > 子 ctx 持有父 ctx 引用(嵌入),但父 ctx 通过 children map 持有子引用。这是一种非对称设计——父到子是显式追踪,子到父通过 Context 嵌入间接访问。删除子节点时从父的 children map 移除以避免内存泄漏。 ### WithValue 的性能坑点 WithValue 有几个需要注意的问题: 1. **每次调用都分配新对象**:`WithValue` 不会修改已有节点,而是创建整个链路的副本路径。链式调用 N 次会产生 N 个 context 对象。 2. **查找复杂度 O(depth)**:`Value()` 沿链路逐层查找匹配的 key。深层嵌套时性能显著下降。 3. **GC 压力**:短期存在的 ctx 虽然存活时间短,但在高 QPS 场景下(如 HTTP handler),大量短期对象会给 GC 带来负担。 4. **key 必须是可比较的**:如果使用 slice/map/function 作为 key,会导致 panic。最佳实践是用自定义不可导出的类型: ```go type userIDKey struct{} ctx := context.WithValue(r.Context(), userIDKey{}, user.ID) // 取值 if id, ok := ctx.Value(userIDKey{}).(string); ok { // ... } ``` ## 代码示例 ### 优雅取消 goroutine 树 ```go func fetchData(ctx context.Context) ([]byte, error) { resp, err := http.Get("https://api.example.com/data") if err != nil { return nil, fmt.Errorf("HTTP failed: %w", err) } defer resp.Body.Close() data, err := io.ReadAll(resp.Body) select { case <-ctx.Done(): return nil, ctx.Err() // 请求被取消 default: return data, nil } } ``` 在关键操作前后检查 ctx.Done(),确保即使网络操作完成也能及时响应取消信号。 ### Timeout 与 Deadline 的实际应用 ```go func handleRequest(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, 5*time.Second) defer cancel() // 必须调用,释放资源 result, err := heavyComputation(ctx) if err != nil { return fmt.Errorf("computation: %w", err) } return process(result) } ``` `defer cancel()` 是关键——没有它,context 及其关联的 timer goroutine 永远不会被释放,导致 goroutine leak。 ### WithValue 的正确用法 ```go func logMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { traceID := generateTraceID() ctx := context.WithValue(r.Context(), traceIDKey{}, traceID) next(w, r.WithContext(ctx)) } } ``` 在 Web 场景中,ctx 最常见的用途是携带 trace ID、用户认证信息等跨中间件需要的元数据。 ## 实践场景 ### 面试高频问题 **Q: Context 什么时候应该被传递给函数?** 原则是:只要你的函数可能在长时间运行后被中断,就应该接受 Context 作为第一个参数。这是 Go 社区的标准规范。 **Q: 可以用 Context 存储数据库连接吗?** 不应该。数据库连接的生命周期独立于单次请求的上下文,存储在 struct 字段或通过依赖注入管理更合适。 **Q: Context 的 value 能被并发安全地读取吗?** 可以。一旦 context 创建完成,它的 key-value 对就不可变了,天然线程安全。但 value 本身的类型需要保证并发安全。 ### 实战建议 - **always pass context as first parameter**: `(ctx context.Context, ...) -> (...)` - **always defer cancel when you create a derived context**: `defer cancel()` 是不可省略的习惯 - **never store context in a struct**: context 描述的是单次操作的上下文,不适合持久化 - **use context for cancellation, not data transfer**: 优先用 struct 字段传递业务数据 ## 扩展阅读 - [[Goroutine 调度模型]] — Context 取消信号由调度器驱动的 goroutine 退出机制配合使用 - [[Select 多路复用机制]] — select-case 中 `<-ctx.Done()` 是最常见的超时控制模式 - [[Sync 包核心源码]] — waitgroup 与 context 常组合使用以协调批量 goroutine 退出