深入解析Golang之Context

​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接口。类型结构如下

深入解析Golang之Context_第1张图片

函数、结构体和变量说明

名称 类型 可否导出 说明
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节点是不可被取消的节点,所以它就不存在取消说法。就是当父节点被取消的时候,它下面所有的子节点都会被取消。

深入解析Golang之Context_第2张图片

// 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。

深入解析Golang之Context_第3张图片

// *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中移除掉,因为父级节点并没有取消。

深入解析Golang之Context_第4张图片

执行ctx5.cancel前

深入解析Golang之Context_第5张图片

执行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退出,防止泄露。

深入解析Golang之Context_第6张图片

// 查找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。

深入解析Golang之Context_第7张图片

// 从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")会不断查询父节点,直到第二个父节点,查到结果返回。

深入解析Golang之Context_第8张图片

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复制一份到新的,在新的上面修改。

     

  • 深入解析Golang之Context_第9张图片

  • 对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

 

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

 

 

你可能感兴趣的:(golang,后端,Context,编程语言,go语言,golang)