前言
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不存储实际业务键值cancelCtxKey是context包中定义的私有标记键,标识当前context是否为cancelCtxcancleCtx.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 - 若父
context是cancleCtx类型,将子context加入父context的childrenset中(cancleCtx所实现的cancle()已经确保了父子取消一致性 ) - 若父
context实现了afterFuncer接口,通过该接口注册一个取消所有可取消的子context的回调函数,回调函数会在父context取消时自动调用。stop用于停止回调函数的调用,同时将当前context的父context替换为以原父context和stop重新封装的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}
}
- 若父
context或key为空或key不可比较,触发panic - 否则返回以给定键值对构建的
valueCtx
写在最后
注:本文参考了公众号小徐先生的编程世界的文章
作者水平有限,如有错误,敬请指正

