[Golang] 万字详解,深入剖析context

前言

Golang是一门原生支持高并发的语言,由于其goroutine轻量级的特性,应用程序会在运行过程中大量地创建和销毁goroutine,那么主goroutine如何控制其创建的子goroutine的生命周期呢?

context通过在goroutine之间传递取消信号、截止时间等信息,很好的处理了主goroutine对子goroutine协作式生命周期控制。

此外, context还具备一定的数据存储能力,实现共享资源在整个调用链路间的传递。

思考

goroutine context

goroutine可以派生,context也可以派生,那么当goroutine遇上context,会碰撞出怎样的火花呢?

版本声明

本文中涉及的源码为go 1.25.1版本,不同的版本源码实现可能略有差异

一图总览

核心数据结构

1 Context

接口声明

go 复制代码
type Context interface {
    Deadline() (deadline time.Time, ok bool)
   
    Done() <-chan struct{}
   
    Err() error

    Value(key any) any
}

Context是一个接口,定义了4个核心api:

  • Deadline:返回过期时间和布尔值,若布尔值为false则表示永不过期
  • Done:返回一个只读的channel,若该channel在当前goroutine中可读,则表明父context向当前子context传递了退出信号
  • Err:返回当前子context被取消的原因
  • Value:返回当前context中绑定的值,入参为该值的key

2 emptyCtx

结构体声明及接口实现

go 复制代码
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是一个Context接口的实现类型,本质是一个空的context,就像一张空白的画布,具有以下性质:

  • 永不取消
  • 无键值对
  • 无过期时间

3 cancelCtx

结构体声明

go 复制代码
type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      atomic.Value          // set to non-nil by the first cancel call
    cause    error                 // set to non-nil by the first cancel call
}

// ...
// 声明canceler接口
type canceler interface {
    cancel(removeFromParent bool, err, cause error)
    Done() <-chan struct{}
}
  • cancelCtx是可取消上下文的核心结构,其匿名嵌入了Context接口,并实现了部分方法
  • canceler接口的实现类型
  • 声明互斥锁mu,保护字段操作线程安全
  • done是存储 chan struct{} 类型的 "取消信号通道",在重载的Done()确保该字段的类型为chan struct{}
  • children是通过map key的无序,不重复特性实现的set,指向所有子context,值类型struct{}为占位符,并不实际存储值
  • err存储取消的表层原因
  • cause存储取消的底层原因,比err更详细,用于错误链追踪

核心方法实现逻辑详解

Done()

go 复制代码
// ...
//实现Context接口和canceler接口中的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{})
}

为方便理解代码逻辑,此处放一张来自公众号小徐先生的编程世界的流程图

  • c.done.Load() 读取c.done中的取消信号通道并以变量d存储起来
  • d非空,表明当前cancelCtx已取消,直接返回;若获取锁并双重校验后仍为空,构建新的取消信号通道并返回

双重校验的原因分析:

假设现在有两个goroutine A和B,当A在获取锁的过程中,B已经完成了新取消信号通道的构建并已返回,那么在A获取锁后,原c.done的值已经不为空,这时就不应再创建取消信号通道。

Err()

go 复制代码
// ...
// 实现Context接口中的Err()
func (c *cancelCtx) Err() error {
    // An atomic load is ~5x faster than a mutex, which can matter in tight loops.
    if err := c.err.Load(); err != nil {
       return err.(error)
    }
    return nil
}
  • c.err.Load()读取c.err中的值并赋值给err变量
  • err不为空,返回err; 若为空,返回nil

Value()

go 复制代码
// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int

// ...
// 实现Context接口中的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)
       }
    }
}
  • cancleCtx不存储实际业务键值
  • cancelCtxKeycontext包中定义的私有标记键,标识当前context是否为cancelCtx
  • cancleCtx.Value() 依据传入键类型的不同,分为两大核心功能:
    • 传入内部私有标记键cancelCtxKey :判断当前context是否为可取消类型,获取 cancelCtx 实例以执行级联取消、读取取消原因等操作
    • 传入自定义业务键:自下而上,由子及父逐层匹配对应的值,直至找到值或溯源到根上下文结束

cancel()

go 复制代码
// ...
// 实现canceler接口中的cancel()
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if err == nil {
       panic("context: internal error: missing cancel error")
    }
    if cause == nil {
       cause = err
    }
    c.mu.Lock()
    if c.err.Load() != nil {
       c.mu.Unlock()
       return // already canceled
    }
    c.err.Store(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 {
       // NOTE: acquiring the child's lock while holding parent's lock.
       child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
       removeChild(c.Context, c)
    }
}
  • removeFromParent决定是否将当前上下文从其父上下文的子上下文列表中移除
  • err不能为空,避免无意义的空取消
  • c.err为空,表明当前上下文已取消
  • 将当前上下文的取消信号通道置为关闭状态
  • 级联取消所有子上下文

propagateCancel()

为方便理解代码逻辑,此处同样放一张来自公众号小徐先生的编程世界的流程图

go 复制代码
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
    c.Context = parent

    done := parent.Done()
    if done == nil {
       return // parent is never canceled
    }

    select {
    case <-done:
       // parent is already canceled
       child.cancel(false, parent.Err(), Cause(parent))
       return
    default:
    }

    if p, ok := parentCancelCtx(parent); ok {
       // parent is a *cancelCtx, or derives from one.
       p.mu.Lock()
       if err := p.err.Load(); err != nil {
          // parent has already been canceled
          child.cancel(false, err.(error), p.cause)
       } else {
          if p.children == nil {
             p.children = make(map[canceler]struct{})
          }
          p.children[child] = struct{}{}
       }
       p.mu.Unlock()
       return
    }

    if a, ok := parent.(afterFuncer); ok {
       // parent implements an AfterFunc method.
       c.mu.Lock()
       stop := a.AfterFunc(func() {
          child.cancel(false, parent.Err(), Cause(parent))
       })
       c.Context = stopCtx{
          Context: parent,
          stop:    stop,
       }
       c.mu.Unlock()
       return
    }

    goroutines.Add(1)
    go func() {
       select {
       case <-parent.Done():
          child.cancel(false, parent.Err(), Cause(parent))
       case <-child.Done():
       }
    }()
}
  • 该方法在父子context间传递取消信号,确保父context取消,其派生出的所有可取消子context一并被取消
  • 若父context永不取消,直接返回
  • 若父context已被取消,立即复用cancler.cancel()取消所有可取消的子context
  • 若父contextcancleCtx类型,将子context加入父contextchildren set中( cancleCtx所实现的cancle()已经确保了父子取消一致性 )
  • 若父context实现了afterFuncer接口,通过该接口注册一个取消所有可取消的子context的回调函数,回调函数会在父context取消时自动调用。stop用于停止回调函数的调用,同时将当前context的父context替换为以原父contextstop重新封装的stopCtx
  • 若以上条件均不满足,则采用兜底策略:开启一个goroutine,同时监听父子context的取消信号通道。若父context被取消,主动取消所有可取消的子context;若子context被取消,新建goroutine生命结束,函数结束
  • goroutines.Add(1)context 包内部的计数逻辑,用于跟踪当前活跃的 goroutine 数量

stop存在的意义:

假设A和B为父子context,A为父,B为子。由于注册了取消所有可取消的子context的回调函数,当A被取消时,B会被回调函数取消。倘若B已经被取消,那么在A取消前,需调用stop停止回调函数,避免B被二次重复取消。

4 timerCtx

结构体声明

go 复制代码
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}
  • 继承cancelCtx
  • timer是一个时间控制器,到了指定的时刻会触发取消当前上下文的操作
  • deadline记录timerCtx的过期时刻

核心方法实现逻辑详解

Deadline()

go 复制代码
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}
  • 返回取消时刻和状态

cancel()

go 复制代码
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
    c.cancelCtx.cancel(false, err, cause)
    if removeFromParent {
       // Remove this timerCtx from its parent cancelCtx's children.
       removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
       c.timer.Stop()
       c.timer = nil
    }
    c.mu.Unlock()
}
  • 复用cancelCtx.cancel()手动取消timerCtx
  • removeFromParent参照cancelCtx.cancel(),不再赘述
  • timer非空,停止计时,避免到指定时间重复取消
  • c.timer = nil释放对时间控制器的引用,回收资源

5 valueCtx

结构体声明

go 复制代码
type valueCtx struct {
    Context
    key, val any
}
  • 在嵌入Context接口的基础上新增键值对字段
  • valueCtx是唯一存储键值对,用于存储业务数据的context
  • 只适合存放少量作用域较大的全局 meta 数据

核心方法实现逻辑详解

Value()

go 复制代码
func (c *valueCtx) Value(key any) any {
    if c.key == key {
       return c.val
    }
    return value(c.Context, key)
}
  • 若传入key与当前valueCtx的key相匹配,返回当前valueCtx的val
  • 否则向上溯源,参照cancelCtx.Value()

对外暴露的常用函数

下面用以表格的形式呈现context包下对外暴露常用的函数:

函数名称 作用
Background() 返回一个空的、不可取消的根上下文
TODO() 返回一个空的 "占位符" 上下文
WithCancel() 返回一个可手动取消的上下文和一个取消函数
WithDeadline() 返回一个到指定时刻自动取消的上下文和一个取消函数
WithTimeout() 返回一个经过指定时长后自动取消的上下文和一个取消函数
WithValue() 返回一个携带指定键值对的上下文

1 Background()

先放源码:

go 复制代码
type backgroundCtx struct{ emptyCtx }

// ...
func Background() Context {
    return backgroundCtx{}
}

可以看到Background()直接返回了backgroundCtx,在其中又封装了emptyCtx,本质上返回了一个空的、不可取消的根上下文,由该上下文派生并控制其他子上下文的生命周期。

2 TODO()

go 复制代码
type todoCtx struct{ emptyCtx }

// ...
func TODO() Context {
    return todoCtx{}
}

Background()的实现类似,返回todoCtx,又在todoCtx中封装了emptyCtx

当不清楚要使用哪个上下文,或者上下文尚不可用时(因为周围的函数尚未扩展以接受上下文参数),使用TODO()创建根上下文。

3 WithCancel()

先看源码

go 复制代码
type CancelFunc func()

// 对外暴露的表层函数
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
}
  • 若父context为空,则panic
  • 初始化空的cancelCtx,并通过propagateCancel()绑定父子取消信号传播关系
  • 返回cancelCtx的指针和对应的取消函数

4 WithDeadline()

源码如下:

go 复制代码
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    return WithDeadlineCause(parent, d, nil)
}

// ...
// WithDeadlineCause behaves like [WithDeadline] but also sets the cause of the
// returned Context when the deadline is exceeded. The returned [CancelFunc] does
// not set the cause.
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) {
       // The current deadline is already sooner than the new one.
       return WithCancel(parent)
    }
    c := &timerCtx{
       deadline: d,
    }
    c.cancelCtx.propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
       c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
       return c, func() { c.cancel(false, Canceled, nil) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err.Load() == nil {
       c.timer = time.AfterFunc(dur, func() {
          c.cancel(true, DeadlineExceeded, cause)
       })
    }
    return c, func() { c.cancel(true, Canceled, nil) }
}
  • 核心功能:根据传入的父context,派生出子context,同时返回取消函数
  • 校验父context是否为空
  • 若父context的取消时刻在传入取消时刻之前,返回cancleCtx及其取消函数
  • 以传入的取消时刻构建timerCtx,并绑定父子取消信号传递关系
  • dur 是当前时刻到取消时刻的时间间隔长度
  • dur<0,表明指定取消时刻已过,立即取消派生的timerCtx,并返回
  • 若还未到指定时刻,注册回调取消函数,在经过dur时长后执行

5 WithTimeout()

go 复制代码
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}
  • 本质上是WithDeadline(),将传入的时间参数转换为绝对时刻调用WithDeadline()

6 WithValue()

go 复制代码
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}
}
  • 若父contextkey为空或key不可比较,触发panic
  • 否则返回以给定键值对构建的valueCtx

写在最后

注:本文参考了公众号小徐先生的编程世界的文章

作者水平有限,如有错误,敬请指正

相关推荐
mtngt1111 小时前
AI DDD重构实践
go
Grassto2 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉6 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo7 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go
码界奇点8 天前
基于Gin与GORM的若依后台管理系统设计与实现
论文阅读·go·毕业设计·gin·源代码管理
迷迭香与樱花8 天前
Gin 框架
go·gin
只是懒得想了8 天前
用Go通道实现并发安全队列:从基础到最佳实践
开发语言·数据库·golang·go·并发安全