context翻译成中文就是上下文,在软件开发环境中,是指接口之间或函数调用之间,除了传递业务参数之外的额外信息,像在微服务环境中,传递追踪信息traceID, 请求接收和返回时间,以及登录 *** 作用户的身份等等。本文说的context是指golang标准库中的context包。Go标准库中的context包,提供了goroutine之间的传递信息的机制,信号同步,除此之外还有超时(timeout)和取消(cancel)机制。概括起来,Context可以控制子goroutine的运行,超时控制的方法调用,可以取消的方法调用。
为什么需要context根据前面的Context的介绍,Context可以控制goroutine的运行,超时、取消方法的调用。对于这些功能,有没有别的实现方法。当然是有的,控制goroutine的运行,可以通过select+channel的机制实现,超时控制也可以通过ticker实现,取消方法调用也可以向channel中发送信号,通知方法退出。既然Context能实现的功能,也有别的方式能够实现,那为啥还要Context呢?在一些复杂的场景中,通过channel等方式控制非常繁琐,而采用Context可以很方便的实现上述功能。场景1:主协程启动了m个子协程,分别编号为g1,g2,...gm。对于g1协程,它又启动了n个子协程,分别编号为g11,g12,...g1n。现在希望主协程取消的时候或g1取消的时候,g1下面的所有子协程也取消执行,采用channel的方法,需要申请2个channel, 一个是主协程退出通知的channel,另一个是g1退出时的channel。g1的所有子协程需要同时select这2个channel。现在是2层,用channel还能接受,如果层级非常深,那监控起来需要很多的channel, *** 作非常繁琐。采用Context可以简单的达到上述效果,不用申请一堆channel。场景2: 在微服务中,任务A运行依赖于下游的任务B, 考虑到任务B可能存在服务不可用,所以通常在任务A中会加入超时返回逻辑,需要开一个定时器,同时任务A也受控于父协程,当父协程退出时,希望任务A也退出,那么在任务A中也要监控父协程通过channle发送的取消信息,那有没有一种方式将这两种情况都搞定,不用即申请定时器又申请channel,因为他们的目的都是取消任务A的运行嘛,Context就能搞定这种场景。
context源码解析下面的源码解析的是go的最新版本1.14.2
结构图
context定义了2大接口,Context和canceler
, 结构体类型*emptyCtx,*valueCtx实现了Context接口,*cancelCtx同时实现了Context接口和cancelr接口,*timerCtx内嵌了cancelCtx,它也间接实现了Context和canceler接口。类型结构如下
函数、结构体和变量说明
名称 | 类型 | 可否导出 | 说明 |
---|---|---|---|
Context | 接口 | 可以 | Context最基本接口,定义了4个方法 |
canceler | 接口 | 不可以 | Context取消接口,定义了2个方法 |
emptyCtx | 结构体 | 不可以 | 实现了Context接口,默认都是空实现,emptyCtx是int类型别名 |
cancelCtx | 结构体 | 不可以 | 可以被取消 |
valueCtx | 结构体 | 不可以 | 可以存储key-value信息 |
timerCtx | 结构体 | 不可以 | 可被取消,也可超时取消 |
CancelFunc | 函数 | 可以 | 取消函数签名 |
Background | 函数 | 可以 | 返回一个空的Context,常用来作为根Context |
Todo | 函数 | 可以 | 返回一个空的 context,常用于初期写的时候,没有合适的context可用 |
WithCancel | 函数 | 可以 | 理解为产生可取消Context的构造函数 |
WithDeadline | 函数 | 可以 | 理解为产生可超时取消Context的构造函数 |
WithTimeout | 函数 | 可以 | 理解为产生可超时取消Context的构造函数 |
WithValue | 函数 | 可以 | 理解为产生key-value Context的构造函数 |
newCancelCtx | 函数 | 不可以 | 创建一个可取消的Context |
propagateCancel | 函数 | 不可以 | 向下传递 context 节点间的取消关系 |
parentCancelCtx | 函数 | 不可以 | 找到最先出现的一个可取消Context |
removeChild | 函数 | 不可以 | 将当前的canceler从父Context中的children map中移除 |
background | 变量 | 不可以 | 包级Context,默认的Context,常作为顶级Context |
todo | 变量 | 不可以 | 包级Context,默认的Context实现,也作为顶级Context,与background同类型 |
closedchan | 变量 | 不可以 | channel struct{}类型,用于信息通知 |
Canceled | 变量 | 可以 | 取消error |
DeadlineExceeded | 变量 | 可以 | 超时error |
cancelCtxKey | 变量 | 不可以 | int类型别名,做标记用的 |
Context接口
Context具体实现包括4个方法,分别是Deadline、Done、Err和Value,如下所示,每个方法都加了注解说明。
// Context接口,下面定义的四个方法都是幂等的
type Context interface {
// 返回这个Context被取消的截止时间,如果没有设置截止时间,ok的值返回的是false,
// 后续每次调用对象的Deadline方法是,返回的结果都和第一次相同,即具有幂等性
Deadline() (deadline time.Time, ok bool)
// 返回一个channel对象,在Context被取消时,此channel会被close。如果没有被
// 取消,可能返回nil。每次调用Done总是会返回相同的结果,当channel被close的时候,
// 可以通过ctx.Err获取错误信息
Done() <-chan struct{}
// 返回一个error对象,当channel没有被close的时候,Err方法返回nil,如果channel被
// close, Err方法会返回channel被close的原因,可能是被cancel,deadline或timeout取消
Err() error
// 返回此cxt中指定key对应的value
Value(key interface{}) interface{}
}
canceler接口
canceler接口定义如下所示,如果一个Context类型实现了下面定义的2个方法,该Context就是一个可取消的Context。Context包中结构体指针*cancelCtx和*timerCtx
实现了canceler接口。
为啥不将这里的canceler接口与Context接口合并呢?况且他们定义的方法中都有Done方法,可以解释得通的说法是,源码作者认为cancel方法并不是Context必须的,根据最小接口设计原则,将两者分开。像emptyCtx和valueCtx不是可取消的,所以他们只要实现Context接口即可。cancelCtx和timerCtx是可取消的Context,他们要实现2个接口中的所有方法。
WithCancel提供了创建可取消Context方法,它有2个返回值,分别是Context类型和func()类型,Context(第一个返回值)在使用时一般会传给其他协程,第二个返回值放在main协程或顶级协程中处理,实现了调用方caller和被调方callee隔离。callee只管负责收到caller发送的取消信息时执行退出 *** 作。
// canceler接口,核心是cancel方法,Done()不能省略,propagateCancel中的child.Done()
//在使用,因为Context接口中已有Done()方法了,它们的签名是一模一样的
// context包核心的两个接口是这里的canceler和前面的Context接口,为啥不把这里的canceler与
// Context合成一个接口呢?
// 1. 这里可以看到作者的设计思想,cancel *** 作不是Context必须功能,像*valueCtx
// 只是传递数据信息,并不会有取消 *** 作。
// 2. WithCancel提供给外部唯一创建*cancelCtx函数非常巧妙,它的返回值有2部分,分别是
// Context类型和func()类型,这样显示的将Context的取消 *** 作放到取消函数中(第二个返回值)
// Context(第一个返回值)会传给其他协程,第二个返回值放在main协程或顶级协程处理取消
// caller只管负责取消,callee只关心取消时做什么 *** 作,caller通过发送消息通知callee。
// canceler是不可导出的,外部不能直接 *** 作canceler类型对象,只能通过func() *** 作。
// *cancelCtx和*timerCtx实现了该接口
type canceler interface {
cancel(removeFromParent bool, err error)
// 这里的Done()不能省略,propagateCancel中的child.Done()在使用
Done() <-chan struct{}
}
Background/Todo
background和todo是两个全局Context,实现方式都是返回nil值,两者都不可导出,通过包提供的Background()和TODO()导出供外部使用,两者都是不可取消的Context,通常都是放在main函数或者最顶层使用。
type emptyCtx int
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 interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
// background和todo是两个全局Context,实现方式都是返回nil值
// 两者都不可导出,通过包提供的Background()和TODO()导出供外部使用
// 两者都是不可取消的Context,通常都是放在main函数或者最顶层使用
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
cancelCtx
cancleCtx结构字段比emptyCtx丰富多了,它内嵌了Context接口,在golang中,内嵌也就是继承,当我们将一个实现了Context的结构体赋值给cancleCtx的时候,cancelCtx也就实现了Context定义的4个方法。只不过*cancelCtx重写了Done、Err和Value方法。mu字段用于保护结构体中的字段,在访问修改的时候进行加锁处理,防止并发data race冲突。done是一个channel,同关闭close(done)实现信息通知,当一个channel被关闭之后,它返回的是该类型的nil值,本处就是struct{}。children字段保存可取消的子节点,cancelCtx可以级联成一个树形结构,如下图所示:当B被取消的时候,挂在它下面的G也会被取消,E节点是不可被取消的节点,所以它就不存在取消说法。就是当父节点被取消的时候,它下面所有的子节点都会被取消。
// cancelCtx是可取消的Context, 当它被取消的时候,它的孩子cancelCtx也都会被取消,也就是级联取消
type cancelCtx struct {
Context
// 互斥锁字段,保护下面字段,防止存在data race
mu sync.Mutex // protects following fields
// done表示是否取消标记,当done被取消,也就是close(done)之后,调用cancelCtx.Done()
// 会直接返回
done chan struct{} // created lazily, closed by first cancel call
// 记录可取消的孩子节点
children map[canceler]struct{} // set to nil by the first cancel call
// 当done没有取消即没有关闭的时候,err返回nil, 当done被关闭时,err返回非空值,err值的内容
// 反映被关闭的原因,是主动cancel还是timeout取消
err error // set to non-nil by the first cancel call
}
*cancelCtx.Value方法返回的是cancelCtx的自身地址,只有当可被取消的类型是context中定义的cancelCtx时,才会被返回,否则,递归查询c.Context.Value,直到最顶级的emptyCtx,会返回nil。结合下面的图很好理解,ctx4.Value(&cancelCtxKey)会返回它本身的地址&ctx4。对于ctx3.Value(&cancelCtxKey),因为它是valueCtx, 结合valueCtx.Value(key)源码可以看到,它的key不可能是&cancelCtxKey,因为在包外是不能获取到cancelCtxKey地址的,它是不可导出的,会走到ctx3.Context.Value(&cancelCtxKey),就是在执行ctx2.Value(&cancelCtxKey), ctx2是cancelCtx,所以会返回ctx2的地址&ctx2。
// *cancelCtx.Value方法看起来比较奇怪,将key与一个固定地址的cancelCtxKey比较
// cancelCtxKey是不可导出的,它是一个int变量,所以对外部包来说,调用*cancelCtx.Value
// 并没有什么实际意义。它是给内部使用的,在parentCancelCtx中有如下使用
// p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
// 可以看到传入的key是cancelCtxKey的地址,那key==&cancelCtxKey肯定是成立的嘛
// 所以直接返回*cancelCtx。理顺一下思路,就是*cancelCtx调用Value返回它本身,非*cancelCtx
// 调用Value是它自己的实现,肯定跟*cancelCtx是不一样的,对非*cancelCtx调用c.Context.Value(&cancelCtxKey)
// 会一直递归查询到最后的context(background/todo),返回的会是nil。
// 总结出来,*cancelCtx.Value并不是给外部使用的,它主要表示当前调用者的Context是一个*cancelCtx
func (c *cancelCtx) Value(key interface{}) interface{} {
if key == &cancelCtxKey {
return c
}
return c.Context.Value(key)
}
Done方法用于通知该Context是否被取消,通过监听channel关闭达到被取消通知目的,c.done没有被关闭的时候,调用Done方法会被阻塞,被关闭之后,调用Done方法返回struct{}。这里采用惰性初始化的方法,当c.done未初始化的时候,先初始化。
// 初始化的时候 *cancelCtx.done是未初始化的channel, 所以它的值是nil, 这里判断如果它是
// nil表明channel done还未初始化,先进行初始化。如果已初始化,返回的是c.done的值。这里有2点
// 对于新手值得学习,1是c.done先赋值给一个临时变量,return 的是临时变量,不能直接return c.done
// 因为这样c.done会处于c.mu锁之外,未起到保护作用。2是这里采用惰性初始化方式,新创一个*cancelCtx的
// 时候没有理解初始化,在使用*cancelCtx.Done中进行的初始化
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
cancel方法通过关闭*cancelCtx.done达到通知callee的目的。如果c.done还未初始化,说明Done方法还未被调用,这时候直接将c.done赋值一个已关闭的channel,Done方法被调用的时候不会阻塞直接返回strcut{}。然后递归对子节点进行cancel *** 作,最后将当前的cancelCtx从它所挂载的父节点中的children map中删除。注意removeFromParent参数,对所有子节点进行cancel的时候,即下面的child.cancle(false,err)传递的是false,都会执行c.children=nil做清空 *** 作,所以没有必要传true, 在最外层cancel funtion被cancel的时候,removeFromParent要传true,这里需要将cancelCtx从它的父节点children中移除掉,因为父级节点并没有取消。
执行ctx5.cancel前
执行ctx5.cancel后
// 取消 *** 作,通过关闭*cancelCtx.done达到通知的效果,WithCancel函数调用的时候
// 返回一个context和cancel function,cancel function是一个闭包函数,关联了外层
// 的context,当 cancel function被调用的时候,实际执行的是 *cancelCtx.cancel函数
// 将*cancelCtx.done关闭,callee调用context.Done会返回,然后对挂在下面的children
// canceler执行递归 *** 作,将所有的children自底向上取消。
// note: 这里在递归取消子canceler的时候,removeFromParent传递参数为false, 为啥这样写呢?
// 因为这里所有子canceler的children都会执行c.children=nil,做清空 *** 作,所有没有必要传true
// 进行removeChild(c.Context,c) *** 作了。
// 在最外层cancel function调用cancel的时候,removeFromParent要传true, 这里需要将*cancelCtx
// 从它的父级canceler中的children中移除掉,因为父级canceler并没有取消
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
// 子*cancelCtx不用执行removeChild() *** 作,自底向上递归清理了children.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
查找child的挂载点,找到第一个*cancelCtx,将child挂在它下面,如果父节点都是不可取消的,那就不存在挂载点,直接返回。还有一种情况,找到了可取消的Context,但这个Context不是cancelCtx, 这种可取消的Context是我们自定义结构体类型,它是没有children的。对应下面的单独开启一个goroutine的代码,监听parent.Done,当parent被取消的时候,取消下面的子节点,即child.cancel。child.Done是不能省略不写的,当child取消的时候,这里启动的groutine退出,防止泄露。
// 查找child的挂载点,如果父级Context都是不可取消的,直接返回,因为不存在这样的挂载点
// 从parent中沿着父级向上查找第一个*cancelCtx,找到了就将child添加到
// p.children中,如果没有找到*cancelCtx, 但是一个别类型的可取消Context,启动一个
// goroutine单独处理
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
// 走到这里表示找到了一个可取消的Context(done非nil), 但这个可取消的Context
// 并不是*cancelCtx, 那这个Context是啥呢?它可能是我们自己实现的可取消的Context类型
// 他是没有children map 字段的,当它被取消的时候,要通知子Context取消,即要执行child.cancel
// 这里的 case <- parent.Done()不能省略
go func() {
select {
// 这里的parent.Done()也是不能省略的,当parent Context取消的时候,要取消下面的子Context child
// 如果去掉,就不能级联取消子Context了。
case <-parent.Done():
// 因为父级Context并不是*cancelCtx,也就不存在p.children, 不用执行removeChild操作,
// 这里直接传false
child.cancel(false, parent.Err())
// 当child取消的时候,这里启动的groutine退出,防止泄露
case <-child.Done():
}
}()
}
}
parentCancel查找parent的第一个*cancelCtx,如果done为nil表示是不可取消的Context,如果done为closedchan表示Context已经被取消了,这两种情况可以直接返回,不存cancelCtx了。parent.Value(&cancelCtxKey)递归向上查找节点是不是cancelCtx。注意这里p.done==done的判断,是防止下面的情况,parent.Done找到的可取消Context是我们自定义的可取消Context, 这样parent.Done返回的done和cancelCtx肯定不在一个同级,它们的done肯定是不同的。这种情况也返回nil。
// 从parent位置沿着父级不断的向上查找,直到遇到第一个*cancelCtx或者不存这样的*cancelCtx
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
// done=closedchan 表示父级可取消的Context已取消,可以自己返回了
// done=nil 表示一直向上查找到了顶级的background/todo Context, 也可以直接返回了
if done == closedchan || done == nil {
return nil, false
}
// 递归向上查询第一个*cancelCtx
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
p.mu.Lock()
// 这里为啥要判断 p.done==done, 见源码分析说明
ok = p.done == done
p.mu.Unlock()
if !ok {
return nil, false
}
return p, true
}
removeChild比较简单,将child从parent最先遇到的*cancelCtx中的children map中删除。
// 从parent中找到最先遇到的*cancelCtx, 这个是child的挂载点,将child从最先遇到的*cancelCtx map
// 中删除。
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
timerCtx
timerCtx内嵌有cancelCtx,所以它是一个可取消的Context,此外它有超时定时器和超时截止时间字段,对timer和deadlien的访问,是通过cancelCtx.mu加锁防止data race的。
// timeCtx超时取消Context,内嵌有cancelCtx,所以间接实现了Context接口
type timerCtx struct {
cancelCtx
// 超时定时器
timer *time.Timer // Under cancelCtx.mu.
// 超时截止时间
deadline time.Time
}
WithDeadline是创建timerCtx的构造函数,用于返回一个可超时取消的Context。
// 可以理解为创建超时Context的构造函数,需要传入一个超时接着时间,创建了一个*timeCtx类型
// 通过*timeCtx结构体定义可以看到,它内嵌了一个cancelCtx类型,这里需要注意下,虽然内嵌的
// 是cancelCtx类型,但是他是实现了Context接口的,因为cancelCtx中内嵌有Context,所以
// cancelCtx实现了Context接口,只不过重写了*cancelCtx.Done(), *cancel.Err(), *cancel.Value()实现
// 进一步timerCtx内嵌有cancelCtx,所以timerCtx也实现了Context接口
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// 父级Context的超时时间比d早,直接创建一个可取消的context, 原因是父级context比子
// context先超时,当父级超时时,会自动调用cancel函数,子级context也会被取消了。所以
// 不用单独处理子级context的定时器到时之后,自动调用cancel函数。
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 同cancelCtx的 *** 作相同 ,将当前的c挂到父级ontext节点上
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
// 如果时间已超时,直接取消
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
// 启动一个定时器,在dur时间后,自动进行取消操作
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
Deadline方法返回timerCtx是否设置了超时截止日期,这里始终返回true,因为通过WithTimeout和WithDeadline创建的*timerCtx都设置了超时时间。
// *timeCtx重写了Deadline实现,方法会返回这个
// Context 被取消的截止日期。如果没有设置截止日期,
// ok 的值 是 false。后续每次调用这个对象的 Deadline 方法时,
// 都会返回和第一次调用相同的结果
// note:这里ok为啥直接返回true呢?因为通过创建*timeCtx的两个方法WithDeadline
// 和WithTimeout都设置了*timeCtx.deadline值
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
*timerCtx重写了cancel的cancel方法,除了执行timerCtx.cancelCtx.cancel,将子context取消,然后做定时器的停止并清空 *** 作。
// 取消 *** 作, *timerCtx重写了cancel的cancel, 先会执行*timeCtx.cancelCtx.cancel, 将
// 子级context取消,然后将当前的*timerCtx从父级Context移除掉
// 最后将定时器停止掉并清空
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
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()
}
WithTimeout是对WithDeadline的包装,将timeout转换成了deadline。
// 提供了创建超时Context的构造函数,内部调用的是WithDeadline, 创建的都是*timerCtx类型。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
valueCtx
key-value Context,用于传输信息的Context,key和value的赋值与访问并没有加锁处理,因为不需要,具体原因见*valueCtx.Value处的说明。
// 在协程中传递信息Context, key和value分别对应传递信息的键值对
// Note: 可以看到valueCtx中并没有锁结构对key,value赋值(WithValue函数)和读取(Value函数) *** 作时进行保护
// 为什么不用加锁,原因见*valueCtx.Value处的解析说明。
type valueCtx struct {
Context
key, val interface{}
}
WithValue返回key-value Context,这里传入的key要是可进行比较的。
// WithValue函数是产生*valueCtx的唯一方法,即该函数是*valueCtx的构造函数。
// key不能为空且是可以比较的,在golang中int、float、string、bool、complex、pointer、
// channel、interface、array是可以比较的,slice、map、function是不可比较的,
// 复合类型中带有不可比较的类型,该复合类型也是不可比较的。
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
*valueCtx.Value递归查询key,从当前节点查询给定的key,如果key不存在,继续查询父节点,如果都不存在,一直查询到根节点,根节点通常都是Background/TODO,返回nil。
// Value函数提供根据键查询值的功能,valueCtx组成了一个链式结构,可以理解成一个头插法创建的单链表,
// Value函数从当前的Context查询key,如果没有查到,继续查询valueCxt的Context是否有对应的key ,
// 可以想象成从当前链表节点,向后顺序查询后继节点是否存在对应的key, 直到尾节点(background或todo Context)
// background/todo Value返回的nil
// Value *** 作没有加锁处理,因为传递给子协程的valueCtx进行Value *** 作时,其它协程不会对valueCtx进行修改 *** 作,这个
// valueCtx是这个只读的Context,所以在valueCtx中对key和value的 *** 作没有进行加锁保护处理,因为不存在data race.
func (c *valueCtx) Value(key interface{}) interface{} {
// 要查询的key与当前的valueCtx(c)中的key相同,直接返回
if c.key == key {
return c.val
}
// 否则递归查询c中的Context,如果所有的Context都没有,则最后会走到background/todo Context,
// background/todo Context的Value函数直接返回的是nil
return c.Context.Value(key)
}
valueCtx实现了链式查找。如果不存在,还会向 parent Context 去查 找,如果 parent 还是 valueCtx 的话,还是遵循相同的原则:valueCtx 会嵌入 parent, 所以还是会查找 parent 的Value 方法的,下面的ctx.Value("key1")会不断查询父节点,直到第二个父节点,查到结果返回。
func main() {
ctx := context.Background()
ctx = WithValue(ctx, "key1", "01")
ctx = WithValue(ctx, "key2", "02")
ctx = WithValue(ctx, "key3", "03")
ctx = WithValue(ctx, "key4", "04")
fmt.Println(ctx.Value("key1"))
}
// valueCtx还实现了String() string签名函数,该签名是fmt包中一个接口,也就说
// valueCtx实现了fmt中的print接口,可以直接传参给fmt.Println(valueCtx)进行打印
// 当前也可以直接fmt.Println(valueCtx.String())打印。
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
// stringify只给*valueCtx.String()使用,在*valueCtx.String()函数中,调用了
// stringify(v.val), v.val要么是string类型,要么实现了stringer接口,
// stringer接口定义了一个方法 String() string
// 即v.val要么是string类型, 要么该类型实现了 String() string 方法
func stringify(v interface{}) string {
switch s := v.(type) {
case stringer:
return s.String()
case string:
return s
}
return ""
}
context最佳实践
方法或函数的第一个参数传递context 首参数传递context对象,例如在net包中,是下面这样定义Dialer.DialContext的。
func (d *Dialer) DialContext(ctx context.Context, network, address string) (Conn, error) {
...
}
通常不要将context放到结构体中 使用 context 的一个很好的心智模型是它应该在程序中流动,应该贯穿整个代码。不希望将其存储在结构体之中。它从一个函数传递到另一个函数,并根据需要进行扩展。
使用WithValue的时候注意传递的value是线程安全的 withValue可能在多个goroutine中使用,而*withValue.value在赋值时无需加锁保护,但是要确保对value *** 作的安全性,例如当value是一个map对象时,在每个groutine是不能修改的,那怎么办呢?当需要修改的时候,采用COW技术即写时复制,将原map复制一份到新的,在新的上面修改。
对cancelCtx要记得调用cancel 不是中途放弃的时候,才去调用cancel,只要你的任务完成了,就需要调用cancel,这样Context的资源才能释放。
总结用Context来取消一个goroutine 的运行,这是 Context 最常用的场景之一,Context 也被称为 goroutine 生命周期范围(goroutine-scoped)的 Context,把Context 传递给 goroutine。但是,callee goroutine需要尝试检查 Context 的 Done 是否关闭了 对带超时功能context的调用,比如通过grpc访问远程的一个微服务,超时并不意味着你会通知远程微服务已经取消了这次调用,大概率的实现只是避免客户端的长时间等待,远程的服务器依然还执行着你的请求。
Reference
[1]
深入 Go 并发模型:Context:https://zhuanlan.zhihu.com/p/75556488
[2]
深度解密go语言之context:https://www.cnblogs.com/qcrao-2018/p/11007503.html
欢迎关注微信公众号—数据小冰,更多精彩内容和你一起分享
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)