深入解析Golang之Context

深入解析Golang之Context,第1张

​context是什么

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

 

欢迎关注微信公众号—数据小冰,更多精彩内容和你一起分享

 

 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/995870.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存