一、前言

context 包是 Go 标准库中用于处理请求作用域数据、取消信号和截止时间(deadlines)的核心工具。它最初在 Go 1.7 中引入,旨在标准化跨 API 边界和进程间传递上下文信息的行为。源码中的注释已经很好地概括了其用途:

  • 上下文传播:用于在函数调用链中传递请求相关的元数据、取消信号和超时信息。
  • 并发安全Context 接口的方法可以被多个 goroutine 安全地调用。
  • 典型场景:服务器请求处理、分布式系统调用、超时控制等。

二、Context原理

1、Context的树状结构

树状结构

如图所示,context的连接通常为树状结构,也就是context是向下传递的,图中箭头所指方向为查找值或者传递取消时的方向,最顶端指向的永远都是emptyCtx。

2、Context的实现类型

  • emptyCtx:通常作为父节点存在,表示没有任何取消或者传递值的能力。
  • cancelCtx:可以取消的ctx,支持传递取消但其本身没有传递值的能力。
  • timerCtx: 继承于cancelCtx,在cancelCtx的基础上增加了超时能力。
  • cancelCtx:本身只支持携带一组值。

3、Context保存值的方式采用链式结构而不是map

  • 减少额外开销:使用map会增加系统的开销,降低性能,而且map是非线程安全的也就是需要加锁。
  • 保持层次性:context的设计上是有传递的层次性的,如果使用map则无法体现层次性。
  • map的唯一性:map中的key是唯一的,作为传递型的context,每个子context或者子分支context可以允许保存自己相同key值的context。
  • context的设计之初:本身设计上就是管理协程的生命周期,通常传递和生命周期相关的信息比如traceid,而不是设计上当存储介质使用。

4、使用注意事项

  • 因为contest使用链式存储,也就是每增加一个kv对就需要增加一个context,如果kv对过多时遍历性能就会下降,所以要控制kv的个数。

三、源码分析

一、基础结构与接口定义

1. Context 接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}
  • Deadline:返回截止时间和是否设置了截止时间的标志。返回 (time.Time{}, false) 表示无截止时间。
  • Done:返回一个只读通道,关闭时表示上下文被取消或超时时。返回 nil 表示永不取消。
  • Err:返回取消原因,nil 表示未取消,常见错误有 CanceledDeadlineExceeded
  • Value:获取与某个键关联的值,用于请求作用域数据的传递。

这个接口定义了上下文的最小行为集合,所有具体实现都必须满足这些方法。

2. 错误常量

var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}

type deadlineExceededError struct{}
func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }
  • Canceled:表示主动取消。
  • DeadlineExceeded:表示超时,实现了 TimeoutTemporary 方法,表明这是一个超时相关的临时错误,通过实现error接口来返回定义错误。

二、空上下文:emptyCtx 和其派生类型

1. emptyCtx

type emptyCtx struct{}

func (emptyCtx) Deadline() (deadline time.Time, ok bool) { return }
func (emptyCtx) Done() <-chan struct{}                   { return nil }
func (emptyCtx) Err() error                              { return nil }
func (emptyCtx) Value(key any) any                       { return nil }
  • emptyCtx 是一个永不取消、无截止时间、无值的上下文,作为基础类型。
  • 所有方法都返回默认值(nil 或空值),表示“什么都不做”。
  • 设计上,它是不可变的,作为其他上下文的起点。

2. backgroundCtxtodoCtx

type backgroundCtx struct{ emptyCtx }
func (backgroundCtx) String() string { return "context.Background" }

type todoCtx struct{ emptyCtx }
func (todoCtx) String() string { return "context.TODO" }

func Background() Context { return backgroundCtx{} }
func TODO() Context       { return todoCtx{} }
  • backgroundCtx:用于程序的主上下文、初始化或测试场景,是所有上下文树的根节点。
  • todoCtx:占位符,表示开发者尚未确定使用哪个上下文。
  • 两者的唯一区别是 String() 方法,用于调试或日志输出。
  • 使用方式:通常代码中要考虑对接上游ctx时可以使用TODO创建临时父节点,后面代码完整后使用传入ctx即可。

三、可取消上下文:cancelCtx(核心ctx)

1. 结构体定义

type cancelCtx struct {
    Context              // 嵌入父上下文
    mu       sync.Mutex  // 保护以下字段的并发访问
    done     atomic.Value // 存储 chan struct{},惰性延迟初始化,原子操作保证并发安全
    children map[canceler]struct{} // 子上下文集合,nil 表示已取消
    err      error       // 取消原因
    cause    error       // 具体的取消原因(可选)
}
  • Context:嵌入父上下文,形成树形结构。
  • mu:互斥锁,确保并发安全(例如多个 goroutine 同时取消)。
  • done:使用 atomic.Value 存储通道,避免锁竞争下的重复初始化。
  • children:记录子上下文,用于取消传播。
  • errcause:分别记录标准错误和具体原因(causeWithCancelCause 引入的扩展)。

2. 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{})
}
  • 延迟初始化:第一次调用时检查 done 是否已存在,若不存在则加锁创建。
  • 原子操作:使用 atomic.Value 避免竞争,确保线程安全。
  • 重用性:后续调用直接返回已创建的通道。

3. Err() 方法

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}
  • 加锁读取:确保并发安全。
  • err:在未取消时为 nil,取消后为 CanceledDeadlineExceeded

4. Value() 方法

func (c *cancelCtx) Value(key any) any {
    if key == &cancelCtxKey {
        return c
    }
    return value(c.Context, key)
}

func value(c Context, key any) any {
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        case *cancelCtx:
            if key == &cancelCtxKey {
                return c
            }
            c = ctx.Context
        case withoutCancelCtx:
            if key == &cancelCtxKey {
                // This implements Cause(ctx) == nil
                // when ctx is created using WithoutCancel.
                return nil
            }
            c = ctx.c
        case *timerCtx:
            if key == &cancelCtxKey {
                return &ctx.cancelCtx
            }
            c = ctx.Context
        case backgroundCtx, todoCtx:
            return nil
        default:
            return c.Value(key)
        }
    }
}
  • 特殊键:如果查询的是 &cancelCtxKey,返回自身(用于 Cause 函数),这里的key == &cancelCtxKey其实是内部的协议,再获取value是调用者函数会根据是否要返回cancelctx传递key类型。
  • 递归查找:否则委托给父上下文。

5. 可取消的上下文:WithCancel(外部创建Cancelctx的方法)

WithCancel 创建一个可取消的上下文,返回一个 Context 和一个 CancelFunc

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := withCancel(parent)
    return c, func() { c.cancel(true, Canceled, nil) }
}

func withCancel(parent Context) *cancelCtx {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    c := &cancelCtx{}
    c.propagateCancel(parent, c)
    return c
}

取消传播propagateCancel 方法将子上下文与父上下文关联。如果父上下文被取消,子上下文也会被取消。

6. propagateCancel:取消传播的核心

func (c *cancelCtx) propagateCancel(parent Context, child canceler) {//注意这里参数为什么还要传入child,为什么不直接使用接收者
    c.Context = parent // 进来先关联父节点。注意锁的细节,并没有进来就加锁,这里完全保证了只有当前协程可以访问。这里也可以看出为什么context设计时要用链式结构
    
    done := parent.Done() // 这里使用就是定义时注释里面写的惰性创建,节省资源。
    if done == nil {
        return // 父上下文永不取消,不需要传播
    }
    select {
    case <-done: //利用关闭chan的特性
        child.cancel(false, parent.Err(), Cause(parent)) // 父上下文已取消,调用自己的取消方法
        return
    default:
    }
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            child.cancel(false, p.err, p.cause)
        } else {
            if p.children == nil { // 这里也是使用惰性创建的思想,在需要时才创建
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
        return
    }
    goroutines.Add(1) // 这里仅用于测试,注释中有描述
    go func() { // 如果父上下文不支持直接关联,但是又实现了done接口,通常是自定义context,则启动一个 goroutine 监听(这里的goroutine会不会无法结束的风险,什么时候会有?)
        select {
        case <-parent.Done():
            child.cancel(false, parent.Err(), Cause(parent))
        case <-child.Done():
        }
    }()
}

func parentCancelCtx(parent Context) (*cancelCtx, bool) { // 这里没有使用加锁的方式,而是使用了原子操作保证了并发的性能
    done := parent.Done()    //获取父节点的done通道
    if done == closedchan || done == nil { //如果是已经cancel的或者空的说明父节点不支持取消,这里closedchan使用init函数直接关闭
        return nil, false
    }
    p, ok := parent.Value(&cancelCtxKey).(*cancelCtx) //这里通过cancelCtxKey协议来获取cancelctx自身,注意这里巧妙的使用了内存地址来保证key的唯一性,如果使用cancelCtxKey,为int类型,则用户层使用时key则不能使用0,不然就会冲突,使用地址就不会有此问题。
    if !ok {
        return nil, false
    }
    pdone, _ := p.done.Load().(chan struct{}) //获取成员done类型atomic.Value中的数据,因为done为atomic.Value,所以使用Load方法进行获取
    if pdone != done {    //这里什么时候会不相等,在注释中说如果cancelctx被包装之后会不想等,源码中x_text.go有对此种情况的测试
        return nil, false
    }
    return p, true
}

// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})

func init() {
    close(closedchan)
}
  • 逻辑分支

    1. 父上下文永不取消done == nil,直接返回,因为父节点是不支持取消的类型,则不需要传递取消到父节点。
    2. 父上下文已取消:立即取消子上下文。
    3. 父上下文是 cancelCtx:将子上下文加入 children,由父上下文管理。
    4. 其他情况:启动 goroutine 监听父上下文的 Done 通道,这里注意创建完ctx后最好要调用或者defer调用返回的cancel函数,如果父节点是自己实现的接口则可能出现协程泄露的问题。
  • 取消逻辑:调用 cancel 方法关闭 done 通道,设置 errcause,并递归取消所有子上下文。

7. cancel:取消逻辑

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    if cause == nil { //可以使用withcause来传递自定义的错误
        cause = err
    }
    c.mu.Lock() // 锁的位置在这里,原因为cancel是向下传递的,也就是说可能会从多个位置传递到这里
    if c.err != nil {
        c.mu.Unlock()
        return // 已取消
    }
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan)
    } else {
        close(d)
    }
    for child := range c.children {
        child.cancel(false, err, cause)
    }
    c.children = nil //告诉gc进行内存回收
    c.mu.Unlock()
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {
    if s, ok := parent.(stopCtx); ok {
        s.stop()
        return
    }
    p, ok := parentCancelCtx(parent) //判断父节点是否支持取消
    if !ok {
        return
    }
    p.mu.Lock()
    if p.children != nil {
        delete(p.children, child)
    }
    p.mu.Unlock()
}
  • 幂等性:如果已取消,直接返回。
  • 状态更新:设置 errcause,关闭 done 通道。
  • 传播:递归取消所有子上下文,清空 children
  • 清理:若 removeFromParent 为真,从父上下文移除自己。

四、带截止时间的上下文:timerCtx

1. 结构体定义

type timerCtx struct {
    cancelCtx
    timer    *time.Timer // 定时器
    deadline time.Time   // 截止时间
}

2. WithDeadline 创建

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    return WithDeadlineCause(parent, d, nil)
}

func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        return WithCancel(parent) // 父截止时间更早,直接返回一个取消ctx
    }
    c := &timerCtx{deadline: d}
    c.cancelCtx.propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded, cause) // 已超时
        return c, func() { c.cancel(false, Canceled, nil) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded, cause)
        })
    }
    return c, func() { c.cancel(true, Canceled, nil) }
}
  • 截止时间检查:若父上下文的截止时间更早,直接返回 WithCancel
  • 定时器设置:若未超时,创建 time.Timer 在到期时触发取消。
  • 立即超时:若 dur <= 0,立即取消。
  • timerCtx:嵌入了 cancelCtx,增加了 timerdeadline 字段。
  • 超时处理:通过 time.AfterFunc 设置定时器,到期后自动调用 cancel

WithTimeout 只是 WithDeadline 的包装,利用 time.Now().Add(timeout) 计算截止时间。

3. cancel 方法

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
    c.cancelCtx.cancel(false, err, cause)
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}
  • 扩展:在 cancelCtx.cancel 的基础上停止定时器,避免资源泄漏。

五、带值的上下文:valueCtx

1. 结构体定义

type valueCtx struct {
    Context
    key, val any
}

2. 创建逻辑

func WithValue(parent Context, key, val any) Context {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if key == nil {
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}
  • 约束:键必须是可比较的,避免冲突。

3. Value 方法

func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}
  • 查找:匹配当前键返回对应值,否则递归查询父上下文。

5. 不随父取消的上下文:WithoutCancel

WithoutCancel 创建一个即使父上下文取消也不会被取消的上下文:

func WithoutCancel(parent Context) Context {
    return withoutCancelCtx{parent}
}

type withoutCancelCtx struct {
    c Context
}

func (withoutCancelCtx) Done() <-chan struct{} { return nil }
func (withoutCancelCtx) Err() error            { return nil }

这在某些需要独立生命周期的场景中很有用。


六、其他辅助功能

1. Cause 函数

func Cause(c Context) error {
    if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok {
        cc.mu.Lock()
        defer cc.mu.Unlock()
        return cc.cause
    }
    return c.Err()
}
  • 用途:获取上下文的具体取消原因,若无则返回 Err()

2. AfterFunc

func AfterFunc(ctx Context, f func()) (stop func() bool) {
    a := &afterFuncCtx{f: f}
    a.cancelCtx.propagateCancel(ctx, a)
    return func() bool {
        stopped := false
        a.once.Do(func() { stopped = true })
        if stopped {
            a.cancel(true, Canceled, nil)
        }
        return stopped
    }
}
  • 延迟执行:上下文取消后在 goroutine 中运行 f
  • 停止机制:通过 sync.Once 确保只执行一次。

七、应用场景与示例

1. 请求超时控制

func slowOperationWithTimeout(ctx context.Context) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
    defer cancel() // 释放资源
    select {
    case <-time.After(200 * time.Millisecond): // 模拟慢操作
        return "Done", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    ctx := context.Background()
    result, err := slowOperationWithTimeout(ctx)
    fmt.Println(result, err) // 输出: "" context deadline exceeded
}

2. 取消信号传播

func worker(ctx context.Context, ch chan string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Worker stopped:", ctx.Err())
            return
        case ch <- "Working":
            time.Sleep(100 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    ch := make(chan string)
    go worker(ctx, ch)
    fmt.Println(<-ch) // 输出: Working
    cancel()          // 取消上下文
    time.Sleep(1 * time.Second)
}

3. 请求作用域数据

type userKey int

const key userKey = 1

func processRequest(ctx context.Context) {
    user, ok := ctx.Value(key).(string)
    if ok {
        fmt.Println("User:", user)
    }
}

func main() {
    ctx := context.WithValue(context.Background(), key, "Alice")
    processRequest(ctx) // 输出: User: Alice
}

4. 分布式系统中的上下文传递

在微服务中,上下文可以携带请求 ID、认证信息等,通过 HTTP 头或 gRPC 元数据传递。


八、总结

context 包通过树形结构和通道机制实现了上下文的传播与管理。cancelCtx 处理取消,timerCtx 添加超时,valueCtx 携带数据,配合辅助函数如 CauseAfterFunc,形成了一个强大而灵活的工具。源码的每一部分都经过精心设计,确保性能、并发安全和资源管理的平衡,代码中对并发的时机进行了精确控制,什么时候使用原子变量而不是用锁保证并发的性能,什么时候加锁,加锁控制到什么力度。各对象之间的继承关系,父对象要实现的方法等等都是值得深入学习的地方。

--EOF

最后修改:2025 年 02 月 25 日
如果觉得我的文章对你有用,请随意赞赏