context 简介

为什么要用 context 呢?因为在并发编程中,一般我们可以通过在主协程中 close 掉 unbuffered channel 来对子协程进行生命周期的管控,但是如果协程中又有协程,然后子协程中又有协程,不断嵌套,使用 close channel 的方式来管控所有子协程生命周期就显得很复杂了,所以有没有一种方式能够很好的管理这些子协程的生命周期呢?答案是肯定的,我们需要这样一种机制:

  • 上层任务取消,下层的任务也会被取消掉
  • 中间层任务取消,下层任务会被取消掉,但是上层任务不会被取消掉
  • 并且可以管控住下层任务的超时时间或者截止时间

context 包中的源代码内容其实还是很适合通读一遍的,因为总共代码行数就 500 多行,其中还包含大量的代码行注释

接口 Context

Context 接口定义了上下文的信息,Done chan 是超时控制的关键。Context 对外暴露,用来控制多协程的生命周期

type Context interface {
		// 返回 context 是否会被取消以及自动取消时间(即 deadline)
    Deadline() (deadline time.Time, ok bool)
  	// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
    Done() <-chan struct{}
 	  // 在 channel Done 关闭后,返回 context 取消原因
    Err() error
	  // 获取 key 对应的 value
    Value(key interface{}) interface{}
}
接口 canceler

canceler 接口定义了可被调用者取消下层协程的方法

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}
整型 emptyCtx & 函数 Background & 函数 TODO
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 包中定义了 Background 函数和 TODO 函数,他们会返回一个空的 ctx

background 一般作为根 ctx,todo 一般是作为一个临时 ctx 使用,就是在你手头上没有其他 ctx 可以传递的时候在不知道传递什么 context 时候可以传递 todo,一般在代码重构中会使用,最终会被替换其他 context

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)
func Background() Context {
	return background
}
func TODO() Context {
	return todo
}
结构体 cancelCtx & 函数 WithCancel

通过结构体内部加上 Context 来继承 Context 功能,cancelCtx 是一个不对外暴露的 ctx,它既实现了 Context 的方法,也实现了 canceler 的方法,说明 cancelCtx 是可以被取消的

我们可以注意到一个关键细节,就是 cancelCtx 虽然实现了 canceler 的方法,但是 cancel 方法却不对外暴露,原因是如果对外暴露,就会被处于下层的 ctx 调用 cancel 从而影响上层协程的生命周期,实际情况中生命周期应该是被顶层的 ctx 上下文所管控

type cancelCtx struct {
    Context
    mu       sync.Mutex
    done     chan struct{}
    children map[canceler]struct{}
    err      error
}

如果 cancelCtx 是不对外暴露的,那我们怎么使用它呢,可以使用 WithCancel 函数,传入一个父 ctx 来生成一个可取消的 ctx

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
  // ....
}
结构体 timerCtx & 函数 WithDeadline & 函数 WithTimeout

timerCtx 也是一个不对外暴露的上下文结构,它是包含了 cancelCtx,相当于是对 cancelCtx 的扩展,

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.
    deadline time.Time
}

函数 WithTimeout 实际里头是调用了函数 WithDeadline,函数 WithDeadline 实际返回的就是 timerCtx 类型的 ctx

withDeadline 函数中传递父 ctx,以及 ddl 绝对时间让 ctx 取消

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
  // ...
}

WithTimeout 函数中传递父 ctx,以及相对时间让 ctx 超时取消

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
  return WithDeadline(parent, time.Now().Add(timeout))
}
结构体 valueCtx & 函数 WithValue

valueCtx 也是一个 ctx,实现了 Context 的方法,并且可以附带上

type valueCtx struct {
    Context
    key, val interface{}
}

valueCtx 同样也是不对外暴露的,可以通过 WithValue 函数来拿到 valueCtx 的上下文结构体,传入父 ctx,以及 kv

func WithValue(parent Context, key, val interface{}) Context {
  // ...
}
Context 使用建议

对于 Context 管控 goroutine 生命周期,go 官方有一些建议:

  • 不要将 Context 塞到结构体里。直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx
  • 不要向函数传入一个 nil 的 context,如果你实在不知道传什么,标准库给你准备好了一个 context:todo
  • 不要把本应该作为函数参数的类型塞到 context 中,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等
  • 同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的