什么是context

goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。 context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。最重要的是它是并发安全的。

Context的底层实现是mutex与channel的结合,前者用于初始部分参数,后者用于通信

context底层原理

类型

名称

作用

Context

接口

定义了 Context 接口的四个方法

emptyCtx

结构

实现了 Context 接口,它其实是个空的 context

CancelFunc

函数

取消函数

canceler

接口

context 取消接口,定义了两个方法

cancelCtx

结构体

可以被取消

timerCtx

结构体

超时会被取消

valueCtx

结构体

可以存储 k-v 对

Background

函数

返回一个空的 context,常作为根 context

TODO

函数

返回一个空的 context,常用于重构时期,没有合适的 context 可用

WithCancel

函数

基于父 context,生成一个可以取消的 context

newCancelCtx

函数

创建一个可取消的 context

propagateCancel

函数

向下传递 context 节点间的取消关系

parentCancelCtx

函数

找到第一个可取消的父节点

removeChild

函数

去掉父节点的孩子节点

init

函数

包初始化

WithDeadline

函数

创建一个有 deadline 的 context

WithTimeout

函数

创建一个有 timeout 的 context

WithValue

函数

创建一个存储 k-v 对的 context

整体类图

Context_函数返回

接口

Context

type Context interface {
// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
Done() <-chan struct{}

// 在 channel Done 关闭后,返回 context 取消原因
Err() error

// 返回 context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)

// 获取 key 对应的 value
Value(key interface{}) interface{}
}


​Context​​ 是一个接口,定义了 4 个方法,它们都是​​幂等​​的。也就是说连续多次调用同一个方法,得到的结果都是相同的。

​Done()​​ 返回一个 channel,可以表示 context 被取消的信号:当这个 channel 被关闭时,说明 context 被取消了。注意,这是一个只读的channel。 我们又知道,读一个关闭的 channel 会读出相应类型的零值。并且源码里没有地方会向这个 channel 里面塞入值。换句话说,这是一个 ​​receive-only​​ 的 channel。因此在子协程里读这个 channel,除非被关闭,否则读不出来任何东西。也正是利用了这一点,子协程从 channel 里读出了值(零值)后,就可以做一些收尾工作,尽快退出。

​Err()​​ 返回一个错误,表示 channel 被关闭的原因。例如是被取消,还是超时。

​Deadline()​​ 返回 context 的截止时间,通过此时间,函数就可以决定是否进行接下来的操作,如果时间太短,就可以不往下做了,否则浪费系统资源。当然,也可以用这个 deadline 来设置一个 I/O 操作的超时时间。

​Value()​​ 获取之前设置的 key 对应的 value

canceler

type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}


实现了上面定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:​​*cancelCtx​​ 和 ​​*timerCtx​​。注意是加了 ​​*​​ 号的,是这两个结构体的指针实现了 canceler 接口。

Context 接口设计成这个样子的原因:

  • “取消”操作应该是建议性,而非强制性
    caller 不应该去关心、干涉 callee 的情况,决定如何以及何时 return 是 callee 的责任。caller 只需发送“取消”信息,callee 根据收到的信息来做进一步的决策,因此接口并没有定义 cancel 方法。
  • “取消”操作应该可传递
    “取消”某个函数时,和它相关联的其他函数也应该“取消”。因此,Done() 方法返回一个只读的 channel,所有相关函数监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到。

结构体

emptyCtx

是Context接口的实现

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
}


这实际上是一个空的 context,永远不会被 cancel,没有存储值,也没有 deadline。

var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}

func TODO() Context {
return todo
}


background 通常用在 main 函数中,作为所有 context 的根节点。

todo 通常用在并不知道传递什么 context的情形。例如,调用一个需要传递 context 参数的函数,你手头并没有其他 context 可以传递,这时就可以传递 todo。这常常发生在重构进行中,给一些函数添加了一个 Context 参数,但不知道要传什么,就用 todo “占个位子”,最终要换成其他 context。

cancelCtx

type cancelCtx struct {
Context

// 保护之后的字段
mu sync.Mutex
done chan struct{}
children map[canceler]struct{}
err error
}


这是一个可以取消的 Context,实现了 canceler 接口.它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context

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
}


c.done 是“懒汉式”创建,只有调用了 Done() 方法的时候才会被创建。再次说明,函数返回的是一个只读的 channel,而且没有地方向这个 channel 里面写数据。所以,直接调用读这个 channel,协程会被 block 住。一般通过搭配 select 来使用。一旦关闭,就会立即读出零值。

func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 必须要传 err
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // 已经被其他协程取消
}
// 给 err 字段赋值
c.err = err
// 关闭 channel,通知其他协程
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}

// 遍历它的所有子节点
for child := range c.children {
// 递归地取消所有子节点
child.cancel(false, err)
}
// 将子节点置空
c.children = nil
c.mu.Unlock()

if removeFromParent {
// 从父节点中移除自己
removeChild(c.Context, c)
}
}


cancel() 方法的功能就是关闭 channel:c.done;递归地取消它的所有子节点;从父节点从删除自己。达到的效果是通过关闭 channel,将取消信号传递给了它的所有子节点。goroutine 接收到取消信号的方式就是 select 语句中的读 c.done 被选中

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
这是一个暴露给用户的方法,传入一个父 Context(这通常是一个 background,作为根节点),返回新建的 context,新 context 的 done channel 是新建的.
当 WithCancel 函数返回的 CancelFunc 被调用或者是父节点的 done channel 被关闭(父节点的 CancelFunc 被调用),此 context(子节点) 的 done channel 也会被关闭