版本:go 1.19
在前一篇文章中我们讨论了 go
Context
的一些常见使用方式,今天我们再来从源码的角度深入了解一下
Context
的设计与实现。
Context
的源码数量不多,去掉注释大概只有两三百行,但是包含的信息量巨大(所以本文也比较长),而且设计得非常巧妙,值得读一读。
然后,下面的
图解 propagateCancel
这一小节的几个图描述了
Context
的工作机制,如果不想看代码,可以直接拉到下面。
再了解一下 chan
在开始本文之前,先来了解一下
Context
实现的关键:
chan
,对于
chan
(再准确一点,我们这里讨论的其实是只读
chan
),我们需要清楚以下几点:
<-ch
表示从 chan 中获取值。<-ch
在通道(chan
)尚未关闭的时候,会一直阻塞,直到通道接收到值。所以有时候通过select
语句来监听chan
,从而实现协程间的通信。<-ch
在通道(chan
)关闭了之后,会立即返回,但是返回的是chan
关联类型的零值,如果我们还需要判断是否是因为关闭才返回的话,需要用两个值来接收<-ch
的返回值,如v, ok := <-ch
,ok
表明了通道是否已经关闭,如果是关闭导致它返回,则返回的是false
。
下面这个例子展示了
Context
实现的关键(通过
close(chan)
,所有
<-chan
会返回,本质上来说是一种 “广播机制”):
package main
import("fmt""time")funcmain(){// 创建一个 chan,类型是 struct{}
ch :=make(chanstruct{})gofunc(){select{// 这个 case 会在 chan 关闭或者收到值的时候执行,// 在这里的情况是关闭了 chan。case v, ok :=<-ch:if!ok {// 输出 "chan ch is closed."
fmt.Println("chan ch is closed.")}// 关闭 chan 之后得到的是 ch 的零值,也就是一个空结构体实例
fmt.Println(v)// {}}}()// 关闭 chan,所有从 chan 读取的操作都会立即返回。// 关闭 chan 之后,<-ch 返回的第一个值是 chan 对应类型的零值,第二个参数是 false。// 如果不是关闭的 chan,第二个参数是 true,表示可以从 chan 获取到数据。close(ch)// 防止程序退出看不到效果
time.Sleep(time.Second)// {},chan struct{} 关闭后,从中获取值的时候会立即返回一个空结构体实例
fmt.Println(<-ch)}
在
Context
中,
context.Done()
方法返回的
chan
,不会接收任何的值,但是在调用
CancelFunc
的时候,会关闭这个
chan
,因此
所有的<-context.Done()
会返回一个零值,返回什么不重要,重要的是,它返回的时候就代表着被上游取消了(代表的是一个取消信号)。
Context 的 UML 图
先来看看它的 UML 图,在后面会陆续展开细说。
- 首先,有两个关键的接口,分别是
Context
和canceler
。Context
里面的接口是我们实际开发的时候用的,而canceler
是context
包内部使用, 这个canceler
接口定义了一个cancel
方法,这个方法就是用来发送取消信号的。 emptyCtx
代表一个空的Context
,往往用作根Context
,valueCtx
在父Context
的基础上加了一个键值对。cancelCtx
同时实现了Context
和canceler
接口,表示一个可以取消的Context
。timerCtx
在cancelCtx
的基础上加了一个定时器,表示可以在指定时间之后由定时器进行取消操作。又或者由开发者自行取消。
context 包的结构体、方法说明
Context
接口:定义了Context
接口的四个方法emptyCtx
结构体:一个空Context
CancelFunc
函数类型:Context
的取消函数canceler
接口:Context
取消接口cancelCtx
结构体:实现了取消接口的Context
timerCtx
结构体:超时会取消的Context
valueCtx
结构体:可以存储键值对的Context
Background()
函数:返回空Context
,常作为根Context
TODO()
函数:返回一个空Context
,在需要Context
的地方又没有合适的Context
就用这个WithCancel()
函数:基于父Context
,创建一个可取消的Context
newCancelCtx()
函数:创建一个可取消的Context
propagateCancel()
函数:将节点挂载到上游第一个cancelCtx
上,又或者启动协程监听Context
取消事件parentCancelCtx()
函数:返回上游的第一个cancelCtx
removeChild()
函数:移除Context
节点init()
函数:包初始化函数,创建了一个关闭的chan
WithDeadline()
函数:创建一个有deadline
的Context
WithTimeout()
函数:创建一个有timeout
的Context
WithValue()
函数:创建一个存储键值对的Context
Context 接口
type Context interface{// 返回一个 channel,当 context 被取消或者到了 deadline 的时候,// 这个 channel 会被 close,从而 <-chan struct{} 会返回。// 在没有关闭之前,一直阻塞,因为不会有任何地方往这个 channel 中发送值。Done()<-chanstruct{}// 在 channel Done 返回的 channel 关闭后,返回 context 取消原因。Err()error// 返回 context 是否会被取消以及自动取消时间(即 deadline)// ok 为 true,表明设置了 deadline,第一个返回值就是设置的 deadline// ok 为 false,表示没有设置 deadline,第一个返回值没意义。Deadline()(deadline time.Time, ok bool)// 获取 key 对应的 valueValue(key interface{})interface{}}
Context
接口定义了 4 个方法,它们都是幂等的,也就是说连续多次调用同一个方法,得到的结果都是相同的。
Deadline()
示例:
package main
import("context""github.com/davecgh/go-spew/spew""time")funcmain(){
ctx,_:= context.WithTimeout(context.Background(), time.Second)// 输出 ctx 的 deadline,具体时间为 1 秒之后
spew.Dump(ctx.Deadline())
ctx1 := context.Background()// ctx1 的超时时间是一个零值
spew.Dump(ctx1.Deadline())// 输出:// (time.Time) 2022-11-19 11:45:38.702281 +0800 CST m=+1.000233039// (bool) true// (time.Time) 0001-01-01 00:00:00 +0000 UTC// (bool) false}
我们通过
Deadline()
方法可以知道当前拿到的
Context
参数是否设置了 deadline,以及 deadline 是什么时候,
从而决定接下来是否还需要做一些操作,如果时间太少的话,就可以考虑不做了,因为最终的结果还是超时。
canceler 接口
先看源码:
type canceler interface{cancel(removeFromParent bool, err error)Done()<-chanstruct{}}
实现了
canceler
两个方法的
Context
,就表明该
Context
是可取消的。
cancel
方法的第一个参数
removeFromParent
表示的是,是否从父
Context
移除自身,这是因为
Context
是一个树状结构。
在
Context
取消的时候,它会给所有派生的
Context
也发送取消信号,所以派生新的
Context
的时候会记录从当前
Context
派生出去的
Context
。
但同样的,在
Context
被取消的时候,父
Context
也就再也不需要给这个
Context
发送取消信号啥的。
我们可以看这个图,
Context
派生出了三个
Context
,当
child 3
这个
Context
cancel
的时候,只会影响到
child 3-1
和
child 3-2
以及其自身,
cancel
之后,根结点的
Context
再发送取消信号,
child 3
就再也收不到了,因为它已经从这棵树中移除。
emptyCtx 结构体
emptyCtx
本身没有什么实际的作用,一般用作根
Context
,比如在
main
函数里面创建的
Context
。
type emptyCtx intfunc(*emptyCtx)Deadline()(deadline time.Time, ok bool){return}func(*emptyCtx)Done()<-chanstruct{}{returnnil}func(*emptyCtx)Err()error{returnnil}func(*emptyCtx)Value(key any) any {returnnil}func(e *emptyCtx)String()string{switch e {case background:return"context.Background"case todo:return"context.TODO"}return"unknown empty Context"}
我们使用
context.Background()
或者
context.TODO()
的时候返回的就是一个
emptyCtx
:
var(
background =new(emptyCtx)
todo =new(emptyCtx))funcBackground() Context {return background
}funcTODO() Context {return todo
}
emptyCtx
永远不会被取消,也没有值和 deadline。
TODO
用在需要
Context
但又没有合适的
Context
可以用的时候。
cancelCtx 结构体
cancel
的操作实际上只会做一次,后续调用
cancel
的时候会返回第一次
cancel
的结果,
cancel
是一个幂等操作。
// 可以被取消,取消的时候,所有实现了 canceler 接口的派生出来的 Context 也会被取消。type cancelCtx struct{// cancelCtx 也实现了 `Context` 接口
Context
// mu 用以保护后面的 done、children、err 字段
mu sync.Mutex
// 是一个 chan struct{},懒汉式创建,// 在第一次 cancel 的时候被关闭
done atomic.Value
// 记录所有可以取消的子 Context// 在第一次 cancel 的时候会被设置为 nil。
children map[canceler]struct{}// 在第一次 cancel 的时候会被设置为非 nil 的值
err error}
cancelCtx.done
是一个支持原子操作的
chan struct{}
。
先来看看它的
Done()
方法实现:
// 返回一个只读的 chan,但没有任何地方会往这个 chan 写入数据,// cancel 的时候会关闭这个 chan,从而任何 <-ch 的操作都会立即返回。func(c *cancelCtx)Done()<-chanstruct{}{// 如果 done 这个 chan 已经初始化了,就直接返回。
d := c.done.Load()if d !=nil{return d.(chanstruct{})}// 如果 done 还没初始化,则会进行初始化。// 也就是上面说的 "懒汉式" 的创建方式,只有在需要的时候才会初始化。
c.mu.Lock()defer c.mu.Unlock()
d = c.done.Load()if d ==nil{
d =make(chanstruct{})
c.done.Store(d)}return d.(chanstruct{})}
再来看看
Err
方法:
func(c *cancelCtx)Err()error{// 使用 mu 保证并发安全,本质是 return c.err
c.mu.Lock()
err := c.err
c.mu.Unlock()return err
}
cancel 方法
然后再来看
cancel
方法的实现:
cancel
方法做了如下操作:
- 关闭
c.done
- 取消
c
的所有孩子Context
- 如果
removeFromParent
为true
,会将c
从其父Context
的children
属性中移除
// 发送取消信号func(c *cancelCtx)cancel(removeFromParent bool, err error){// 必须传递一个 errif err ==nil{panic("context: internal error: missing cancel error")}// 如果已经取消,直接返回。(幂等的设计)
c.mu.Lock()if c.err !=nil{
c.mu.Unlock()return// already canceled}// 记录取消原因,在调用 c.Err() 的时候会返回这个原因
c.err = err
// 关闭 done 这个通道,通知其他协程
d,_:= c.done.Load().(chanstruct{})if d ==nil{
c.done.Store(closedchan)}else{close(d)}// 遍历它的所有子结点,并对其子结点进行取消操作for child :=range c.children {
child.cancel(false, err)}// 将子结点置空
c.children =nil
c.mu.Unlock()// 从父结点中移除自己if removeFromParent {removeChild(c.Context, c)}}
WithCancel 方法
我们再来看看创建
cancelCtx
的方式:
// 返回值里的 Context 的 Done 方法返回的 channel 关闭或者 parent 被 cancel 的时候,// 返回值的 CancelFunc 会被执行。funcWithCancel(parent Context)(ctx Context, cancel CancelFunc){// 必须从其他 Context 派生if parent ==nil{panic("cannot create context from nil parent")}
c :=newCancelCtx(parent)// 将 c 挂靠到 parent 的 children 属性中,// 从而在 parent 取消的时候,可以感知得到。propagateCancel(parent,&c)// 具体实现后面有详细说明 return&c,func(){ c.cancel(true, Canceled)}}// 创建一个 cancelCtx 实例funcnewCancelCtx(parent Context) cancelCtx {return cancelCtx{Context: parent}}
我们看到这里的
CancelFunc
的里面调用
cancel
的时候,第一个参数是
true
,这表示在取消的时候,需要从
parent
中移除自身。
parentCancelCtx 方法
parentCancelCtx
方法用以从
parent
开始直到根节点的路径搜索第一个
cancelCtx
,会跳过中间的
valueCtx
。
// 返回当前节点到根节点路径上的第一个 *cancelCtx。// 如果 parent 就是 *cancelCtx,那么返回的就是 parent。// 如果不是,它会从当前结点往 Context 树根结点遍历,找到父结点中的第一个 *cancelCtx,假设是 p。// 然后检查 p.done 是否跟 parent.Done() 一样,不一样的话意味着 *cancelCtx 已经被包装在自定义实现中了,// 这个时候,我们不应该绕过它,直接返回 nil 和 false。// (注意:如果是我们的结构体嵌套了 Context,那么一样会被当做普通的 Context 处理。)funcparentCancelCtx(parent Context)(*cancelCtx,bool){// 判断 parent 的 done 是否已经关闭或者并没有 done chan。// 如果是,则返回 nil 和 false
done := parent.Done()// done 为 nil 表示 parent 或者到根 Context 这条路径上并没有 *cancelCtx(只有 valueCtx 或 emptyCtx)。if done == closedchan || done ==nil{returnnil,false}// 判断 parent 是否是一个 cancelCtx// 如果不是,则返回 nil 和 false// 讲道理,parent.Value(&cancelCtxKey) 的返回值只有两种情况:// emptyCtx(找到根节点也没找到)或者 *cancelCtx(找到了)// (parent.Value 实现细节见下面的 value 那一小节)
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)if!ok {returnnil,false}// 执行到这里的时候:p 是一个 *cancelCtx// 判断 parent.Done() 和 p.done 是否相等:// 不等则意味着 *cancelCtx 已经被包装在自定义实现中了,这个时候,我们不应该绕过它。// 详细请参考:go issue 28728(google)
pdone,_:= p.done.Load().(chanstruct{})if pdone != done {returnnil,false}return p,true}// 从父结点移除自己(从 parent 移除 child)funcremoveChild(parent Context, child canceler){
p, ok :=parentCancelCtx(parent)if!ok {return}// 从父结点的 children 中移除 child
p.mu.Lock()if p.children !=nil{delete(p.children, child)}
p.mu.Unlock()}
propagateCancel 函数
这个函数会在两个地方调用,一个是
WithCancel
,另一个是
WithDeadline
,它的主要作用是,找到
parent
以及其父级
Context
路径上
第一个
cancelCtx
,目的是,将
child
挂载到找到的这个
cancelCtx
的
children
属性上,从而在这个
cancelCtx
取消的时候,
可以通过遍历
cancelCtx.children
对
child
进行通知。
// 由 parent 往根节点搜索第一个 cancelCtx,如果找到则将 child 写入到 cancelCtx.children 中。// 如果找到的 cancelCtx 自定义了 Done,则启动协程监听 cancelCtx.Done()。 funcpropagateCancel(parent Context, child canceler){// 如果 Context 树上完全不存在 cancelCtx,则直接返回
done := parent.Done()if done ==nil{return// parent is never canceled}// 如果 parent 已经取消,则直接取消 childselect{case<-done:// parent is already canceled
child.cancel(false, parent.Err())returndefault:}// 往根节点搜索第一个 cancelCtxif p, ok :=parentCancelCtx(parent); ok {// 找到了,但是已经取消了,则取消 child
p.mu.Lock()if p.err !=nil{// parent has already been canceled
child.cancel(false, p.err)}else{// 找到了,尚未取消。// 将 child 写入到 p 的 children 属性中。// p.children 是懒汉式创建的。if p.children ==nil{
p.children =make(map[canceler]struct{})}
p.children[child]=struct{}{}}
p.mu.Unlock()}else{// 执行到这里的原因是:// 用户自定义了 Done 通道(跟 parent 不是同一个 done),// 所以不能以父节点路径上的 done 来决定 child 是否取消,// 需要通过启动新协程的方式来监听 Done 通道,从而可以正常取消 parent 的孩子节点。
atomic.AddInt32(&goroutines,+1)gofunc(){select{case<-parent.Done():
child.cancel(false, parent.Err())case<-child.Done():}}()}}
图解 propagateCancel
propagateCancel
的实现可能只看代码不好理解,所以我画了几个图来帮助我们理解:
我们可以看到
propagateCancel
里面有一个
parentCancelCtx
,对于
parentCancelCtx
,下一小节有比较详细的说明。
这个图描述了
parentCancelCtx
的实际执行过程,在我们调用
propagateCancel
的时候,搜索
cancelCtx
的过程:
- 首先,我们知道,
Context
是一个树状结构,每一个Context
都可以派生出子Context
。 - 图中
parentA
是child
到根节点emptyCtx
路径上的第一个cancelCtx
。 parentCancelCtx
拿到的是parentA
,然后将child
写入到parentA
的children
属性中。- 从而在我们手动取消
parentA
的时候,parentA
可以通过遍历children
的方式,告知child
协程取消了。
注意:搜索的时候会跳过
valueCtx
。
**在
child
到
emptyCtx
路径上搜索第一个
cancelCtx
的过程**:
我们取消的过程大概如下图:
我们理解取消的过程的时候,可以忽略掉
Context
树中那些非
cancelCtx
节点,正如上图这样,实际上取消过程只涉及到
parentA
以及
child
,
其他节点如何并不影响。
如果觉得这个图不太好懂,可以再看看这个图:
**用户覆盖了
done
的特殊情况**:
如果用户覆盖了
done
通道,这表明用户想自行控制什么时候
parentB
结束。(也就是说,
parentB
脱离了路径上
cancelCtx
的控制,
也就是假设
parentA
还是
cancelCtx
,在
parentA
取消的时候,
parentB
是收不到信号的,
parentB
收到信号是在其
Done()
返回的通道关闭的时候。)
这个时候因为我们从
parentB
派生出了一个新的
cancelCtx
(
child
),所以
parentB
需要对
child
进行控制,
也就是说在
parentB
取消的时候,也取消
child
。这种情况下,就是通过
propagateCancel
里面的协程里面实现的。
parentCancelCtx 函数
parentCancelCtx
的描述比较晦涩,如果没有实际的例子我们很难看得懂它的意思。里面有一个比较,
是针对
parent.Done()
和
p.done.Load().(chan struct{})
的,源码里面判断如果这两者不一样,则返回
nil
和
false
。
注释里说,如果两者不一样,我们不应该绕过它(bypass it),但是这里的绕过是什么意思呢?我们来看一个例子:
package c
import("testing""time")type A struct{
Context
ch <-chanstruct{}}func(a *A)Done()<-chanstruct{}{return a.ch
}funcTestCancel(t *testing.T){// 创建一个 cancel context
ctx, cancel0 :=WithCancel(TODO())// 创建一个 A 实例// 这个实例可以内嵌了 Context,所以可以当作 Context 使用,// 但是我们覆盖了 Context 本身的 Done 方法。
ch :=make(<-chanstruct{})
a := A{ctx, ch}// 因为我们覆盖了 Done 方法,所以 go 底层会认为是开发者想要// 自行控制协程取消,所以在 WithCancel 的时候并不会把 ctx1// 挂载到 a 的 children 属性下,这样一来,// go 底层只能再启动一个协程来监听 a 的 Done chan,// 从而可以在 a cancel 的时候可以正常通知到 ctx1。
ctx1, cancel :=WithCancel(&a)gofunc(){
time.Sleep(time.Millisecond *10)cancel()}()// ctx2 会写入到 ctx1 的 children 属性中,// 这样就不需要启动新的协程来监测 ctx1 的 done。
ctx2, cancel2 :=WithCancel(ctx1)
time.Sleep(time.Millisecond *20)}
下面这个图描述了上面这个例子中的
Context
树结构:
我们调用了三次
WithCancel
,这三次的效果都不太一样:
- 第一次调用的时候,
parent.Done
返回nil
,这个时候,我们取消只有调用cancel0
这一种途径,也就是手动取消。 - 第二次调用的时候,
parent.Done
返回的不是nil
,但是和 go 语言底层的那个done
属性不一致(一个是A.Done()
,另一个是cancelCtx.done
)。这种情况下,go 底层就知道,开发者自己定义了一个done
通道,这个时候,会需要另外启动一个协程来监听A.Done()
返回的done
,从而可以在A
结束的时候,通知A
的孩子ctx1
。 - 第三次调用的时候,
parent.Done
返回的不是nil
,而且和cancelCtx.done
相等,说明用户没有重写Done
方法,这样就可以直接将ctx2
挂载到ctx1
的children
属性上,而不用另外启动协程来监听ctx1
的done
。
具体怎么实现的可以看上面的
propagateCancel
这一小节。
之所以这样是为了给开发者一定的控制权,如果忽略了用户自定义的
Done
方法,那么可能取消的操作用户就无法控制了。
但我们覆盖Done
方法就是为了可以自主去控制取消的操作。
child 什么时候从父 Context 移除?
如果我们足够细心,就会发现我们在
cancel
的时候,有的地方需要将
child
从
Context
中移除,而有的地方不需要,那什么时候需要呢?
需要移除的情况:
WithCancel
派生出新的Context
的时候,假设叫root
,这个时候派生的这个root
也是可以继续派生出新的Context
的,而这个root
对于它的子孙Context
它就是根节点,所以当root
被取消的时候,它和它的子孙Context
也要被取消了,所以以root
为根节点的子树需要被移除。WithDeadline
里面,当给定的d
其实已经小于当前时间的时候(也就是父Context
已经超时了),这个时候会将刚挂载到父节点的timerCtx
移除,同时返回的CancelFunc
中,cancel
的第一个参数是false
,因为它已经被移除了。
不需要移除的情况:
- 在
propagateCancel
中监测到parent
已经被取消的时候,因为这个时候child
并没有关联上parent
,所以自然也没有移除的这种操作。 - 就是上面提到的第二种情况中,
WithDeadline
的时候就监测到deadline
已经比当前时间小了(超时了)。 - 在
cancelCtx
的cancel
方法里面,遍历cancelCtx
的孩子节点的时候,不需要做移除的操作,因为cancelCtx
本身就需要被从Context
树中移除。 timerCtx
在没有挂载到parent
上就已经过期了。
timerCtx 结构体
timerCtx
是一个带有定时器的
cancelCtx
,我们既可以手动取消,也可以由底层定时器在到达
deadline
的时候进行取消。
// timerCtx 嵌套了 cancelCtx,这表示我们可以手动取消。// 另外还有一个定时器,这个定时器的执行时间定在 deadline 这个时间点,// 一旦时间到了,就会调用 cancelCtx 的 cancel 方法。type timerCtx struct{
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
我们有两种方法来创建
timerCtx
,一个是
WithDeadline
:
// 基于 parent 创建一个 cancelCtx,内嵌到 timerCtx 中。funcWithDeadline(parent Context, d time.Time)(Context, CancelFunc){// 不能基于 nil 创建一个 timerCtxif parent ==nil{panic("cannot create context from nil parent")}// 如果当前设置的超时时间比 parent 设置的超时时间更长,// 那么不用 timerCtx 开启定时器了,因为 parent 会先到期取消,// 这里再启动一个定时器也没有执行的机会了。if cur, ok := parent.Deadline(); ok && cur.Before(d){// The current deadline is already sooner than the new one.returnWithCancel(parent)}// 创建一个 timerCtx
c :=&timerCtx{
cancelCtx:newCancelCtx(parent),
deadline: d,}// 将刚创建的 timerCtx 挂载到父 Context 中propagateCancel(parent, c)// 判断还有多久到达 deadline
dur := time.Until(d)// 如果 deadline 已经过去了,那么直接执行 timerCtx 的 cancel 逻辑,// 同时移除跟父节点的关联。(创建了还没来得及启动定时器就到期了)if dur <=0{
c.cancel(true, DeadlineExceeded)// deadline has already passed// cancel 不再需要从父节点移除自身,上一行已经移除了return c,func(){ c.cancel(false, Canceled)}}// 启动一个定时器,在到达 deadline 的时候执行 cancel 操作。
c.mu.Lock()defer c.mu.Unlock()if c.err ==nil{
c.timer = time.AfterFunc(dur,func(){
c.cancel(true, DeadlineExceeded)})}// 除了定时器之外,返回一个 CancelFunc 给用户提供自行取消的方式。return c,func(){ c.cancel(true, Canceled)}}
另外一个是
WithTimeout
:
WithTimeout
本质上是对
WithDeadline
的调用而已,只不过描述到期时间的方式不一样而已。
WithDeadline
描述的是具体的到期时间,
WithTimeout
描述的是多久以后的时间,两者其实都代表未来的某一个时间点。
funcWithTimeout(parent Context, timeout time.Duration)(Context, CancelFunc){returnWithDeadline(parent, time.Now().Add(timeout))}
valueCtx
// valueCtx 在父 Context 的基础上,带有一对键值对。// 实现了一个 Value 方法,其他方法都是调用 parent 的。type valueCtx struct{
Context
key, val any
}
WithValue 方法
WithValue
方法一般用在请求范围内的数据共享,
WithValue
方法很简单,就是在
parent
的基础上加上了一个
key
和 一个
value
。
// 返回的子 Context 里面有 key val 对funcWithValue(parent Context, key, val any) Context {// parent 不能为 nilif parent ==nil{panic("cannot create context from nil parent")}// key 不能为 nilif key ==nil{panic("nil key")}// key 必须是可以比较的,// 因为在获取值的时候需要进行 key 的比较。if!reflectlite.TypeOf(key).Comparable(){panic("key is not comparable")}// 返回 valueCtxreturn&valueCtx{parent, key, val}}
valueCtx
valueCtx
是从其他
Context
派生出来的,所以内嵌了
Context
接口,同时还有两个字段是
key
和
val
,表示的是父
Context
传递的键值。
type valueCtx struct{
Context
key, val any
}
value 方法
value
方法用以从
Context
中获取对应的值,它会从
Context
树自底向上进行递归搜索,具体来说会有以下几种情况:
- 如果
ctx
是*valueCtx
,则会判断key
是否等于ctx
里面的key
,如果相等,返回ctx.val
。**否则,再去搜索ctx
的父Context
**。 - 如果
ctx
是*cancelCtx
,同时key
是&cancelCtxKey
,则会返回ctx
。**否则,会继续搜索ctx
到根结点这个路径上的第一个cancelCtx
**。 - 如果
ctx
是*timerCtx
,同时key
是&cancelCtxKey
,则会返回ctx.cancelCtx
。**否则,会继续搜索ctx
到根结点这个路径上的第一个cancelCtx
**。 - 如果
ctx
是*emptyCtx
,则会返回nil
。(因为这时候是最顶层的Context
了,也找不到对应的值)。 - 如果都不是以上的几种情况,则有可能是开发者自定义的
Context
实现,则直接返回c.Value(key)
。
它要解决的问题是:
- 获取父级
Context
中WithValue
共享的值。 - 获取父级
Context
中最靠近当前节点的cancelCtx
(非常重要:它的一个很重要的作用是,将当前节点设置为这个cancelCtx
的children
,从而可以实现在这个父级的cancelCtx
取消的时候,当前的Context
可以感知到)。 - 如果是开发者自己实现的
Context
,则直接调用用户自定义的Value
方法。
// 根据 key 从 c 中获取对应的值,会从 Context 树自底向上递归搜索。funcvalue(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*timerCtx:if key ==&cancelCtxKey {return&ctx.cancelCtx
}
c = ctx.Context
case*emptyCtx:returnnildefault:return c.Value(key)}}}
看下面的图可能会更加直观:
这个方法大概做的事情是,从当前的
Context
中查找指定的
key
,如果找不到则递归地从其父级
Context
上找。
但我们最需要关注的是,它这里面
case *cancelCtx
和
case *timerCtx
的逻辑表明了,在我们调用
parent.Value(&cancelCtxKey)
的
时候,实际上获取到的是当前Context
到根
Context
上第一个
cancelCtx
。
一个更简单的图是下面这样的(当然在实际中,会有多个子节点,这里假设都是只有一个子节点):
为什么是通过关闭 chan 的方式取消?
上面我们说了,在
Context
取消的时候,是通过关闭
chan
的方式来实现的,那么为什么要这么做呢?这是因为,如果说要通过往
chan
写入数据的方式来
通知其他子孙
Context
的话,我们就需要有多少个子孙
Context
就要往
chan
里面发多少次,但是如果选择使用
close
的方式的话,
我们就完全不用管派生出了多少个可以
cancel
的
Context
,因为一旦
chan
关闭了,所有的
<-chan
操作立即得以返回,这同样也实现了通信。
但是
close
这种方式无疑更加高明,更加简洁,当然也更加巧妙。
总结
本文主要讲了如下内容:
- 介绍了
context
包的几个Context
结构体,其中emptyCtx
一般用作根Context
,valueCtx
一般用作请求范围内的数据共享,而cancelCtx
给开发者控制下游Context
提供了一种很好的方式,timerCtx
在cancelCtx
的基础上加了一个定时器,时间到会发送取消信号。 timerCtx
有两种取消方式,一种是开发者手动取消,这个和cancelCtx
一致,另外一种方法是到达deadline
的时候,由定时器来取消。- go 里面
Context
取消的时候,是通过关闭chan
的方式来让下游的Context
感知的,因为chan
的工作机制就是如果被关闭则调用<-chan
会立即返回。 - 创建
cancelCtx
的时候,会将派生的Context
挂载到上游Context
中第一个cancelCtx
的children
上,这样在上游取消的时候,这个派生的Context
可以感知得到。 - 上游取消的信号会跨过中间的所有
valueCtx
,传达到下游那些cancelCtx
,在取消的时候,Context
会从Context
树中移除。 - 如果开发者实现了自己的
Done
通道,并且返回的是跟底层cancelCtx
中不一样的done
通道,则会导致 go 底层启动一个协程来监测这个被覆盖的done
通道。 value
也会从Context
树中自底向上搜索,直到根节点。
版权归原作者 张无忌打怪兽 所有, 如有侵权,请联系我们删除。