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

7.8 KiB
Raw Blame History

tags, create time
tags create time
go
golang
go-principle
context
2026-06-07 15:30

Context 底层原理

概述

本文从源码角度解析 Go context 包的四种实现(emptyCtx / cancelCtx / timerCtx / valueCtx),以及取消传播链和值传递链的底层机制。Context 是 Go 并发编程中控制取消和数据共享的核心工具,理解其实现能让你写出更健壮的并发程序。

[!question] ❓ 思考 为什么 WithValue 派生的 context 层层嵌套形成链表,而不是用 map 存储?当父 context 被取消时,子 context 是如何级联收到信号的?

正文

一、Context 的类型体系

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

数据结构

type cancelCtx struct {
    Context               // 嵌入父 context
    mu       sync.Mutex     // 保护以下字段
    done     atomic.Value   // chan struct{},nil 或未关闭 → 未取消;已关闭 → 已取消
    children map[canceler]struct{} // 子 canceler 集合
    err      error          // 取消原因
}

Done 通道的懒汉创建

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 监听它来感知取消信号。

取消逻辑:递归级联

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)  // 从父节点移除自己
    }
}

取消流程:

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

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

数据结构

type valueCtx struct {
    Context       // 父 context
    key, val interface{}  // 当前层的键值对
}

每个 valueCtx 只存一对键值对,多层 WithValue 会形成嵌套链表:

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

查找过程:

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 应避免使用容易碰撞的类型,推荐使用自定义不可比较类型。

// 推荐的 key 定义方式
type ctxKey string
const myKey ctxKey = "my-key"
ctx := context.WithValue(ctx, myKey, value)

五、TimerCtx:定时取消

type timerCtx struct {
    cancelCtx
    timer *time.Timer
    deadline time.Time
}

timerCtx 嵌入了 cancelCtx,额外增加了一个定时器。在 WithDeadline 中:

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 处理父子关联,支持三种场景

关联笔记