
读源码一是为了学习好的代码风格,二是为了对 Go 这门语言能有深入的了解,能成长为一名合格的 Gopher
Context 翻译成中文就是 '上下文' 的意思,准确的说它是 goroutine 的上下文, Go 1.7 开始引入 context,我看大代码是 Go 1.14.6 context 代码位于 go 源码的 src/context 文件中,这个包代码很少并且包含大量的注释,很方便我们阅读源代码
Context 的作用是用来传递 goroutine 之间的上下文信息,包括 取消信号, 超时信号,截止时间,请求信息(session, cookie),控制一批 goroutine 的生命周期. 在 Go 中我们往往使用 channel + select 的方式来控制协成的生命周期。但是对于复杂的场景,比如 Go 中通常一个协程会衍生出很多子协程, 分别处理不同的事情,这些携程往往具有相同生命周期,具有通用的变量,如果 goroutine 的层级较深使用 channel + select 不太方便,这个时候就可以使用 context.
在 context 包中定义了 Context 这种 interface 类型
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} } 这个 interface 包含 Deadline, Done, Err, Value 四个方法 Deadline: 返回 context 是否会被取消,以及取消的时间 Done: 是在 context 被取消或者 deadline 后返回一个被关闭的 channel Err: 在 channel 关闭后,返回 context 取消原因 Value: 用来获取 key 对应的 value
同时在这个包中有一个 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 } emptyCtx 是 context 的一个最小实现,方法很简单,要么直接返回,要么直接返回 nil 。
在 Go 中初始化一个 context,我们经常使用 context.Background() 或者 context.TODO(), 从如下源码中可以看出这两个方法实际上返回的就是一个 emptyCtx
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo } context.Background() 和 context.TODO() 看着除了方法名不一样,其他都是一样的。 在使用中我们需要区分两种 context 的使用场景,context.Background() 通常是用在 main 、测试, 或者最高层的 context (相当于根 context) 的初始化 context 的时候,而 TODO context 则是当我们不清楚使用什么 context 的时候使用
除了 context.Background() 和 context.TODO() 初始化 context 的方法,context 包还为我们提供了如下四个生成 context 的函数
咱们先看看函数签名如下:
WithCancel(parent Context) (ctx Context, cancel CancelFunc) WithDeadline(parent Context, d time.Time) (Context, CancelFunc) WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) WithValue(parent Context, key, val interface{}) Context WithCancel 用于生成一个可取消的 context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) } } 通常我们代码里如下使用
ctx := context.Background() cancel, ctx := context.WithCancel(ctx) 它接受一个 context (父 context),返回一个 context 和一个可取消该 context 的方法,
WithCancel 首先调用了 newCancelCtx 私有方法,生成了一个 cancelCtx 结构体,然后在调用 propagateCancel 方法将 new context 挂载到父 context 上,我们着重看下 newCancelCtx 和 propagateCancel 方法
// newCancelCtx returns an initialized cancelCtx. func newCancelCtx(parent Context) cancelCtx { return cancelCtx{Context: parent} } // 新的结构体,包含一个 Context,和 4 个私有属性 type cancelCtx struct { // 指向的是父 context // context 链类似一个链表,但是 Context 指向是父 context Context // 互斥锁,保证字段的安全 mu sync.Mutex // protects following fields // 在 context 取消后首先关闭该 chan done chan struct{} // created lazily, closed by first cancel call // 从此 context 衍生出的子 context 挂载在这里 children map[canceler]struct{} // set to nil by the first cancel call // cancel 的原因 err error // set to non-nil by the first cancel call } / A canceler is a context type that can be canceled directly. The // implementations are *cancelCtx and *timerCtx. type canceler interface { cancel(removeFromParent bool, err error) Done() <-chan struct{} } newCancelCtx 代码比较少,他一个父 context,返回一个 cancelCtx 类型的 context, 父 context 被赋值到了 cancelCtx 的 Context 字段 需要注意的是 newCancelCtx 返回的是一个 cancelCtx 类型,该 cancelCtx 实现了 canceler interface 的 cancel 和 Done 方法 cancel 方法用来取消这个 context 以及这个 context children map 上的子 context Done 返回的怎是一个关闭的 channel, 用来表示该 context 是否 cancel
cancel 方法源码:
func (c *cancelCtx) cancel(removeFromParent bool, err error) { if err == nil { panic("context: internal error: missing cancel error") } c.mu.Lock() // 该 context 已经 Done if c.err != nil { c.mu.Unlock() return // already canceled } c.err = err // 关闭 channel if c.dOne== nil { c.dOne= closedchan } else { close(c.done) } // 循环取消该 context 的子 context for child := range c.children { // NOTE: acquiring the child's lock while holding parent's lock. child.cancel(false, err) } // 取消子 context 后,将 children 字段置空 c.children = nil c.mu.Unlock() // 如果指定了 removeFromParent = true // 则需要将该 context 从其父 context 的 children map 字段中删除 if removeFromParent { removeChild(c.Context, c) } } // Done 方法则返回该 context 的 done channel // context 关闭后,外部接受到 channel 的 close 信号 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 方法实现特别简单,通过 context 结构体中的 done chan struct{} 这个字段实现的 调用 cancel 方法,本质上就是对该 context 的 done channel 字段执行 close 操作 此外,如果入参 removeFromParent = ture, 会将此 context 从他的父 context 的 children map 上删除
下面是 propagateCancel 方法源码,propagateCancel 方法特别重要 该方法就是将 newCancelCtx 方法生成的新的 cancelCtx 挂在亲父 context 的 children map 上 在执行挂载的时候,如果父 context 已经取消,就将此 context 也取消掉
func propagateCancel(parent Context, child canceler) { done := parent.Done() // step1: 如果 parent 是不可 cancel 的 // 此时直接返回,没有将 child context 挂到 children map 上的必要 // 因为即便挂载上去,因为父 context 不能取消,子 context 也更无法通过父 context 来取消 if dOne== nil { return // parent is never canceled } select { case <-done: // step2: 父 context 被取消,所以需要将此子 context 也取消 child.cancel(false, parent.Err()) return default: } // step3: 获取当前 context 的父 context // parentCancelCtx 是用来获取父 cancelCtx if p, ok := parentCancelCtx(parent); ok { p.mu.Lock() if p.err != nil { // 父 context 是 canceled ( context 如果取消的,err 字段一定不为空) // step3.1: 如果父 context 是已取消的,就需要将子 context 也取消了 child.cancel(false, p.err) } else { // step3.2: 父 context 没有取消,将此子 context 挂到父 context 的 children map 字段 if p.children == nil { p.children = make(map[canceler]struct{}) } p.children[child] = struct{}{} } p.mu.Unlock() } else { // step4: 这个 else 分支是比较难以理解的地方 // 可以理解为,在并发模型下,如果其他 goroutine 将 parent 的 context 改成了一个 cancelCtx // 那么没有这个分支,会出现 parent done 的时候 child 不知道 parent done 信息 // 导致 child context 无法 cancel, child context 控制的相关的 goroutine 就无法结束,出现内存泄漏 atomic.AddInt32(&goroutines, +1) go func() { select { case <-parent.Done(): // 监听父 context 的 cancel 信息 child.cancel(false, parent.Err()) case <-child.Done(): // 如果 child 自身 cancel 就退出 select,避免当前这个 goroutine 内存泄漏 } }() } } propagateCancel 方法是 context 中算是最复杂的一个方法了,它实现的功能是很简单的, 就是将 newCancelCtx 方法生成的新的 cancelCtx 挂在亲父 context 的 children map 上,不过过程中有很多细节处理,只有耐性阅读源码才能准确的理解这些处理上的细节
WithCancel 方法最后一句 return &c, func() { c.cancel(true, Canceled) }返回当前这个 cancelCtx 的 cancel 方法,作为该 context 外部控制该 context 取消的方法
通过 WithCancel 方法的分析,我们知道了 WithCancel 就是接受 context 参数,该参数作为 parent context 生成一个可以取消的 context, 并且会判断 parent context,如果他是一个未没有取消的 cancelCtx 类型的 context,就将当前新生成的 context 挂到 parent context 的 children map 上, 而 context 的是否取取消是通过 context 的 done 字实现的,该字段是 chan struce{} 类型,取消一个 context 本质是将该 context 的 done 字段的的 channel 关闭
如下可见,cancel context 的 Done 方法,可以看出其返回的就是一个 close 的 channel
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 } 看完 WithCancel 后,我们接着看 WithDeadline 底层实现,有了上面 WithCancel 的学习,看 WithDeadline 就比较容易了
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { // 父 context 已经过期,或者父 context 的 deadline 是早于当前 context 的过期时间的 // 就调用 WithCancel 创建一个 cancel context if cur, ok := parent.Deadline(); ok && cur.Before(d) { // The current deadline is already sooner than the new one. return WithCancel(parent) } // 创建一个 timerCtx,timerCtx 实现了 Context 接口 // timerCtx 结构体包含了 (继承了) cancelCtx // 此外还有一个 *time.Timer 类型的 timer 字段和一个 time.Time 类型的 deadline 字段 // timer 字段存储用来执行 deadline 的定时任务 // deadline 是结束时间 c := &timerCtx{ // 创建一个 cancelCtx 的 context cancelCtx: newCancelCtx(parent), // context 的取消时间 deadline: d, } // 这里和 WithCancel 中一样,将 context 挂载到父 context children map 上 propagateCancel(parent, c) // 结束时间已过, 取消当前 context dur := time.Until(d) if dur <= 0 { // 这里的 cancel 是 timerCtx 中实现的 cancel 方法 // 而不是从 cancelCtx 中继承的 cancel,详见下文 c.cancel(true, DeadlineExceeded) // deadline has already passed return c, func() { c.cancel(false, Canceled) } } c.mu.Lock() defer c.mu.Unlock() // 这里是 deadline context 实现的核心 // 创建一个定时任务,到达时间指定的结束时间(d)的时候,执行此任务,将这个 context 取消掉 if c.err == nil { c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) } } func (c *timerCtx) cancel(removeFromParent bool, err error) { // timerCtx 的 cancel 首先调用了 cancelCtx 的 cancel 方法,将此 context 关闭 // 并将 children map 字段上挂的子 context 取消 c.cancelCtx.cancel(false, err) // 如果指定了 removeFromParent = true // 将从父 context 的 children map 上将当前 context 移除 if removeFromParent { removeChild(c.cancelCtx.Context, c) } c.mu.Lock() // 终止 timer 字段上挂载的定时任务 // 因为上面已经主动 cancel 了,所以需要停止当前 context 上的取消任务了 if c.timer != nil { c.timer.Stop() c.timer = nil } c.mu.Unlock() } 从 WithDeadline 源码中我们会发现 with deadline context 的实现很简单 底层的创建和 WithCancel 基本一致,不同点是 WithDeadline 创建出来的 context 提多了一个 deadline 和 timer 字段 deadline 字段用来记录该 context 的结束时间 timer 上面挂了一个定时任务,负责到了指定的 deadline 时执行 cancel 方法,取消当前 context
看完 WithDeadline 的实现,相信大家能想到 WithTimeout 这种 context 的实现了, 将 WithTimeout 的相对时间,转为一个绝对时间就是,WithTimeout 就变成了一个 WithDeadline,context 源码包中也是这么实现的,大家可以自行阅读源码
现在看看 WithValue 的实现, 从 WithValue 源码中,我们可以看到,WithValue 返回了一个 valueCtx,该 valueCtx 是实现了 Context 接口
func WithValue(parent Context, key, val interface{}) Context { // key 不能是 nil if key == nil { panic("nil key") } // key 必须是可比较的,因为在通过 key 来取 value 的时候,需要对比 key 是否相等 // 可以看下文中的 valueCtx 的 Value 方法的实现,方法返回的时候会对 key 进行比较 if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } // 创建一个 valueCtx 类型的 context return &valueCtx{parent, key, val} } // valueCtx 继承了 Context, 新增了 key, value 两个字段 // WithValue(parent Context, key, val interface{}) Context // 这里显而易见,key 、value 两个字段就是用来存储 WithValue 调用的时候传递进来的 key 和 value
type valueCtx struct { Context key, val interface{} } 我们接着看 WithValue context 获取 value 的方法 这个方法实现很简单,需要注意的是获取值操作是递归获取的 先从 context 本身取值,如果取不到会沿着父链向上获取,最终会找到 emptyCtx, 此时返回值是 nil
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } // 递归获取 key 对应的 value return c.Context.Value(key) } 以下建议来自 context 这个包里的详细注释,我做了一个简单翻译,我们在使用中应当遵循这些使用建议
func DoSomething(ctx context.Context, arg Arg) error { // ... use ctx ... } Go 从 1.7 引入了 context,主要用于在 goroutine 之间传递取消信号、截止时间控制、超时时间控制以及一些通用型变量传递, 我读的源码是 Go 1.14.6 版本的,Go context 包代码特别简短,有大量的注释(注释比代码多,哈哈哈),很适合学习大家可以去读一读源码