Golang标准库在1.7版本引入了context,它是goroutine的上下文,包含了goroutine的运行状态、环境等信息。context则主要用来在goroutine之间传递上下文信息,包括:取消信号、超时时间、截止时间、键值对等。
Context是什么
在Golang的Server里,通常每个请求都会启动若干个goroutine同时进行工作:有些用于与数据库建立连接,有些用于调用接口获取数据……
这些goroutine则需要共享这个请求的基本数据,例如最常见的,登录的鉴权token,处理请求的最大超时时间等等。当请求被取消或者是处理时间超过超时时间,这次请求都将被直接抛弃。这时,所有正在为这个请求工作的goroutine都需要快速退出,系统回收相关的资源。
context包就是为了解决这些问题而开发的,当一个请求衍生了很多相互关联的goroutine时,它可以用于解决这些goroutine的退出通知和数据传递等功能。
源码概览
接下来我们将基于go1.24.3版本来进行源码的深入分析。在进行分析之前,让我们首先从全局梳理一下Context源码的结构。其中包含接口与结构体如下:
包含函数、变量和类型如下:
源码剖析
Context
接下来,我们就可以逐一进行源码的分析了。显然,源码的主干为Context interface
,其定义如下:
1 | type Context interface { |
在Context
接口中,定义了四个幂等的方法:
Deadline
方法会返回Context
的过期时间。Done
方法会返回一个channel
,这个channel
会在Context
被取消时被关闭。这是一个只读的channel
,我们知道当读一个关闭的channel
时会读出来相应类型的零值。因此在子协程中,除非这个channel
已经被关闭,否则是不会读出来任何东西的。Err
方法会返回一个错误。如果Done
这个channel
被关闭了,Err
会返回关闭的原因。如果是因为超过截止日期,则会返回DeadlineExceeded
,如果是除此以外的其他原因被取消,则会返回Canceled
。Value
会获取Context
当中的key对应的value,key不存在时则会返回nil。
canceler
接下来看看另一个接口canceler
。
1 | type canceler interface { |
canceler
是一种可以直接被取消的Context
类型。它的实现类包括*cancelCtx
和*timerCtx
。
emptyCtx
1 | type emptyCtx struct{} |
emptyCtx
是Context
的一个实现,也是backgroundCtx
和todoCtx
共同的基础。它永远不会被取消,没有值,也没有过期时间。
backgroundCtx&todoCtx
1 | type backgroundCtx struct{ emptyCtx } |
backgroundCtx
与todoCtx
除了名称不同外,几乎没有任何区别,它们都属于emptyCtx
类型。
Background
方法会返回一个空的Context
,它永远不会被取消,没有值,也没有过期时间。它通常会被用在main
方法、初始化和测试这些场景当中,作为顶层的Context
接收请求。TODO
方法同样会返回一个空的Context
。当不清楚使用哪种Context
,或者是在调用一个需要传递Context
参数但没有其他Context
可使用时,就可以使用context.TODO
。可以理解为不知道用什么的时候就先用它“占个位置”,等到后面再换成具体的Context
。
cancelCtx
接下来看一个重要的Context
:
1 | type cancelCtx struct { |
cancelCtx
实现了canceler
接口,但是并没有实现Context
接口(没有实现Context
中的Deadline
方法),而是将其embed到了结构体当中。可见,它必然是某个Context
的子Context
。它定义了以下的几个字段:
mu
是一把锁,用于实现对其他字段的并发控制。done
实际是chan struct{}
类型,与Context
中的Done
类似,用于反映cancelContext
的生命周期(是否存活)。children
是一个集合,它存放了cancelCtx
的所有子Context
。err
记录了当前cancelCtx
的错误。cause
记录了取消操作的原因。
1 | func (c *cancelCtx) Value(key any) any { |
在Value
方法中,判断了传入的key是否为特定的cancelCtxKey
,如果是的话就返回cancelCtx
自身的指针。cancelCtxKey
被定义出来的目的就是为了定制化一个能够直接返回cancelCtx
自身的key,实现对cancelCtx
类型的快速判断。
如果不是这个特定的key,则会走通用的函数value
取值返回。
1 | func (c *cancelCtx) Done() <-chan struct{} { |
Done
方法的执行流程如下图所示:
- 首先会尝试读取
cancelCtx
中的done channel
,如果已存在的话就直接返回; - 如果不存在,则加锁检查
channel
是否存在,存在则返回。这里做了一个double-check的动作,是为了防止并发情况下在第一次检查和加锁之间这段时间进行了channel的初始化动作导致误判; - 如果还是不存在,那就说明
channel
确实没有被创建过,就可以进行channel
的初始化并返回。这里体现了channel
的懒加载机制,只有当调用Done
方法时才会去创建channel
。
1 | func (c *cancelCtx) Err() error { |
Err
方法的实现就很简单了,直接加把锁去读err
字段。
1 | func (c *cancelCtx) propagateCancel(parent Context, child canceler) { |
propagateCancel
方法的目的是让父Context
被取消的同时,确保子Context
也被取消。并且它会设置子Conext
的父Context
,以便在parent.Done
关闭的同时触发child.cancel
。propagateCancel
方法的执行流程如下所示:
- 首先设置
parent Context
; - 根据
parent.Done
是否为nil
来判断parent
是否可被取消(emptyCtx
的channel
为空)。如果不可被取消也就不需要给child
设置任何机制来监听parent
的取消信号,因此直接return; - 判断
parent
是否为cancelCtx
,如果是,则上锁,并判断其是否已被取消。如果是,则直接将child
取消,如果没有,则将child
加到children
集合中; - 判断
parent
是否实现了afterFuncer
接口,如果是,则将parent
再包裹一层为stopCtx
,并注册一个stop
函数。这个操作会使得在parent
被取消时,同步执行child
注册的回调函数,也就是将其同步取消。stop
则可以取消这个回调函数的执行; - 如果执行到最后,就会直接起一个goroutine监听
parent
的取消信号,在parent
被取消时取消child
。
这里让我们一起思考两个问题:
- 明明在方法的最后是会起一个goroutine去监听
parent
的channel
的,为什么还要在方法最开始的地方再做一次非阻塞式的监听呢?
这里其实是做了一个优化的策略,能够让子Context
尽早地被取消掉。假设我们传入的父Context
是一个cancelCtx
,那就可以发现代码中有三个地方能够监听到父Context
的取消事件:
- 非阻塞式
select
parentCancelCtx
case 当中的parent.Err != nil
判断- 最后兜底的goroutine
通过这三处的判断就可以尽早地发现“父Context
被取消”这一事件,并及时作出处理。
此外,这种策略还能够更好地应对多线程下并发执行取消动作的情况。- 在
parentCancelCtx
这个case当中,为什么在parent
未被取消时只做了一个将child
加入到parent.children
集合的动作?
因为我们这个方法的本质目的是做到当父Context
被取消的同时,确保子Context
也会被取消。而在parent.cancel
被调用的时候是会将children
集合中的所有Context
都取消掉的,因此在这里直接加入即可。
1 | func (c *cancelCtx) String() string { |
String
方法也很简单,调用通用的contextName
方法获取名称,拼接字符串返回。
1 | func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) { |
cancel
是cancelCtx
的核心方法。这个方法有三个入参,removeFromParent
是一个bool,表示是否要将当前的cancelCtx
从父Context
的children
当中删除;err
是cancelCtx
被取消后需要展示的错误;cause
是cancelCtx
被取消的原因。cancel
方法的执行流程如下所示:
- 首先检查参数
err
是否为空,若为空则panic
。检查cause
是否为空,如果为空则让传入的err
也作为cause
; - 加锁;
- 检查当前的
cancelCtx
是否已经被取消了。这里检查的方式是根据err
字段是否为空来进行判断,因为取消会将err
字段进行非空的赋值,因此如果此时已经为非空,则说明已经被取消,解锁返回; - 如果没有被取消,则进行取消操作:将
err
和cause
字段赋值为传入的对应参数。对于channel
,则判断其是否已经被初始化,如果未被初始化,则直接赋值为closedChan
(一个可复用的已关闭的channel
),否则直接关闭该channel
; - 遍历当前
cancelCtx
的子Context
,将它们都采用相同的方式cancel
。此时因为对于每个子child
而言,父Context
,也就是当前的cancelCtx
已经被cancel
掉了,因此传入的removeFromParent
固定为false
; - 解锁;
- 根据传入的
removeFromParent
判断是否需要调用removeChild
将当前cancelCtx
从父Context
的children
中移除掉。
1 | func removeChild(parent Context, child canceler) { |
观察removeChild
方法是如何将cancelCtx
从父Context
的children
中删除的:
- 首先判断
parent
是否为stopCtx
,如果是的话就调用它的stop
方法并返回。这里的stop
方法是用于取消AfterFunc
执行的,我们注册的AfterFunc
为child.cancel
,而此时child
已经被取消了,不需要重复执行; - 如果不是,则调用
parentCancelCtx
方法判断parent
是否为cancelCtx
,也就是看它能不能被取消,如果不能的话就直接返回; - 如果是,并且
parent
存在children
,则加锁并从其中删除当前的child
; - 解锁返回。
1 | func parentCancelCtx(parent Context) (*cancelCtx, bool) { |
parentCancelCtx
中判断了parent
是否为可取消的cancelCtx
。其执行流程如下:
- 根据
parent.Done
判断其是否不可取消done == nil
或者已经取消done == closedchan
,如果是的话则返回false
; - 从当前
parent
开始,沿着Context
链往上查找cancelCtx
; - 如果找到,并且就是上一级的
parent
(因为这里找到的cancelCtx
可能是更上级的),则返回true
;
1 | type CancelFunc func() |
WithCancel
方法是用于获取一个可取消的Context
的public方法,它接收了一个Context
作为parent
,并会通过调用withCancel
来初始化一个cancelCtx
并实现对parent
取消信号的监听。在withCancel
内部,直接调用了cancelCtx.propagateCancel
来实现。返回值除了创建出的cancelCtx
外还有一个CancelFunc
,调用它就会调用内部的cancel
方法将创建出来的cancelCtx
给取消掉。
1 | type CancelCauseFunc func(cause error) |
WithCancelCause
方法与WithCancel
很类似,只是这里多了一个返回值CancelCauseFunc
,让你可以传入一个error
作为Context
取消的原因。
1 | func Cause(c Context) error { |
Cause
方法是为了返回Context
被取消的原因,也就是cause
字段。但其实只有cancelCtx
才有cause
这个字段,更准确地说,对于用户而言,只有WithCancelCause
创建出来的Context
才有特定的cause
,否则对于cancelCtx
而言,err
与cause
值相同,对于其余类型的Context
则根本不存在cause
,直接返回err
。
afterFuncCtx
1 | type afterFuncCtx struct { |
afterFuncCtx
可以看作是对cancelCtx
做了增强,它增加了两个字段:
once
用于确保并发情况下的单次执行,它会被用于实现是否运行f
的控制;f
则是一个回调函数,它会在cancelCtx
被取消时被调用。
1 | type afterFuncer interface { |
afterFuncer
接口中包含一个函数AfterFunc
,它会接收一个函数f
并返回一个函数stop
。函数 f
是会在Context
被取消时调用的回调函数,而 stop
则是用于取消f
的调用的函数。也就是说,f
和stop
中只有一个会被执行,这个控制的实现则依赖于afterFuncCtx.Once
。如果一个Context
实现了AfterFunc
函数,则表示它会采用上述机制来进行控制。
1 | func AfterFunc(ctx Context, f func()) (stop func() bool) { |
AfterFunc
是一个public函数,它会将传入的Context
参数作为parent
,f
作为回调函数,并基于此实例化一个afterFuncCtx
。这个afterFuncCtx
会监听ctx
的取消信号。返回值是一个stop
函数,在这个方法内部它会与f
进行once
锁的争抢,如果获取成功则会继续执行,调用afterFuncCtx.cancel
方法,并返回true;如果返回了false,则说明f
函数会被执行。
1 | func (a *afterFuncCtx) cancel(removeFromParent bool, err, cause error) { |
可以看到,在afterFuncCtx.cancel
当中,实际上与cancelCtx
的取消方式是类似的。只是它在最后会尝试去获取Once
锁,如果获取成功则会起一个goroutine去执行f
回调函数。
1 | type stopCtx struct { |
当父上下文注册了AfterFunc
时,stopCtx
会被用作cancelCtx
的父上下文。它包含了用于取消AfterFunc
方法执行的stop
方法。
withoutCancelCtx
1 | func WithoutCancel(parent Context) Context { |
withoutCancelCtx
在parent
被取消时,它也不会被取消。它没有Deadline
和Err
,Done channel
也是nil
,并且对这个Context
调用Cause
也同样会返回nil
。
这也就意味着,它可以用于当parent
被取消时,执行一些额外的处理逻辑。
timerCtx
1 | type timerCtx struct { |
timerCtx
包含一个timer
和一个deadline
,这个timer
会被cancelCtx.mu
管理。它embed
了一个cancelCtx
用于实现Done
和Err
方法,并且通过停止timer
和调用cancelCtx.cancel
来实现取消。
1 | func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { |
Deadline
和String
的实现都比较简单,不再赘述。
1 | func (c *timerCtx) cancel(removeFromParent bool, err, cause error) { |
在cancel
方法中,直接通过调用内部cancelCtx
的cancel
方法来完成取消,并同时停止了内部的timer
计时器。注意这里在停止timer
的时候使用了mu
来进行并发控制。
那这个timer
里保存的是什么,它会做什么,又是在哪里初始化的呢?让我们接着往下看。
1 | func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { |
这里有两个公开的方法,都可以用于初始化一个timerCtx
,只是其中一个可以自定义Cause
,一个不可以。
对于这个timerCtx
而言,如果它的parent
的deadline
已经比传入的d
要早了,那么就会使用相同的deadline
。当dealine
超过、返回的cancelFunc
被调用或是parent
的Done channel
被关闭时,这个timerCtx
的Done channel
也会被关闭。WithDeadlineCause
方法的执行流程如下所示:
- 参数检查;
- 检查
parent
的dealine
是否比传入的d
更早,如果是的话就直接返回一个WithCancel(parent)
。因为不可能存在parent
还存在但是child
已经被取消的情况,所以这里的返回相当于沿用了parent
的deadline
; - 初始化一个
stopCtx
并对其进行监听; - 检查传入的
deadline
是否已经超过,如果是的话就直接取消刚创建出来的stopCtx
,并且返回; - 如果没有,就上锁,并利用
timer
计时器进行超时回调函数的注册。这里的回调函数也就是会将创建的stopCtx
进行取消; - 返回。
注意这里不同分支返回的CancelFunc
语义不尽相同,可以联系上下文来进行理解。
1 | func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { |
WithTimout
方法比较简单,就是将timeout
转为deadline
后调用WithDeadline
。
valueCtx
1 | type valueCtx struct { |
valueCtx
当中包含了一个键值对,它会为这个key
实现一个Value
方法,并且将其他的方法调用透传到内部的Context
。
1 | func (c *valueCtx) Value(key any) any { |
可以看到,在valueCtx
的Value
方法中,其实也就是做了一下特判,当key为valueCtx
中的key时,返回对应存储的value
,否则调用value
方法进行查找。
1 | func value(c Context, key any) any { |
可以看到,value
中采用了循环的方式,以当前的Context
作为起点不断向上寻找。其中valueCtx
等类型对应存在不同的特殊处理方式。
1 | func WithValue(parent Context, key, val any) Context { |
WithValue
方法会根据传入的参数初始化一个valueCtx
,但在文档当中对key
提出了一些要求:
- 它必须是非空且可比较的;
key
不应该为string
或者是任何其他的内置类型,以避免使用Context
的包之间发生冲突,使用WithValue
的用户应该为key
定义自己的类型;- 为了避免在给接口赋值时进行内存分配,
key
通常需要有具体的类型struct{}
;或者是在导出context key
时,它的类型应该是指针或者接口。
那为什么要提出这些要求呢?它们是为了解决什么问题?
- 对于第一点而言比较容易理解,因为在
valueCtx.Value
当中对key
的判断直接采用了==
的方式,因此字段必须是可比较的; - 不使用内置类型是为了避免冲突。想象这样一个场景:在
package A
当中初始化了一个ctx = WithValue(ctx, "userID", 123)
,然后在package B
当中初始化了另一个ctx = WithValue(ctx, "userID", 234)
。因为Value
是由下到上(子->父)进行查询的,因此此时userID
就会被后来的所覆盖,造成冲突。而如果给key
自定义一个类型,那就能保证其唯一性,避免这种冲突的发生; - 当一个具体的类型赋值给一个接口时,可能会发生内存分配,而对于
struct{}
而言,它是一个特殊类型,不会占用任何存储空间;或者直接将key
定义为指针或接口类型也能减少内存的占用。但是对于接口这种类型而言,需要确保传入的具体值是唯一且可比较的。
1 | func stringify(v any) string { |
由于不想对unicode
产生依赖,因此这里实现的stringify
方法没有使用fmt
,而是直接采用了reflect
的方式来获取字符串。