[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

写在最后

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

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

相关推荐
Daydreamer1 小时前
Trpc配置插件
go
诗意地回家1 小时前
niuhe.conf 配置文件说明
vscode·go
yagamiraito_16 小时前
757. 设置交集大小至少为2 (leetcode每日一题)
算法·leetcode·go
Code_Artist1 天前
robfig/cron定时任务库快速入门
分布式·后端·go
川白1 天前
用 Go 写多线程粒子动画:踩坑终端显示与跨平台编译【含 Windows Terminal 配置 + Go 交叉编译脚本】
go
zhuyasen2 天前
Go 实战:在 Gin 基础上上构建一个生产级的动态反向代理
nginx·go·gin
Tsblns2 天前
从Go http.HandleFunc()函数 引出"函数到接口"的适配思想
go
Schuyler20252 天前
年轻人的第一个 GO 桌面应用:用 Wails 做个学习搭子计时器
go
狼爷3 天前
Go 重试机制终极指南:基于 go-retry 打造可靠容错系统
架构·go
不爱笑的良田3 天前
从零开始的云原生之旅(十六):金丝雀发布实战:灰度上线新版本
云原生·容器·kubernetes·go