一个 http GET 请求进入服务端,调用了 redis,调用了 mysql,开启了几个协程,客户端如果取消了请求,这些操作如何停下来?如果没有 context,操作正常进行,但是这些数据对客户端是无意义的。
Context 的作用是控制一次请求的生命周期,实现协程、请求的超时控制和级联取消。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}Context 的本质是一个接口,需要实现四个方法 Deadline()、Done()、Err()、Value()。
Deadline 方法:返回 context 的截止时间 deadline,ok 判断是否有截止时间。
Done 方法:这是 context 实现协程间通信的重要方法,返回一个只读的 struct{} channel。空 struct 因为不占用内存,非常适合作为信号传递。
Err 方法:返回 context 被 cancel 的原因。
Value 方法:只保存链路的元信息,如 TraceID。
Background() 返回的 emptyCtx 作为地基,是所有 ctx 的根节点。下面开始我们把 ctx 作为实现了 context 接口的实例,如 emptyCtx、cancelCtx 和 timerCtx。
cancelCtx, cancel := context.WithCancel(parentCtx)
defer cancel()项目中我们常用 context.WithCancel(ctx) 获取一个 cancelCtx,它以参数中 ctx 作为父 ctx。
type cancelCtx struct {
Context
mu sync.Mutex
done atomic.Value
children map[canceler]struct{}
err atomic.Value
cause error
}只需要了解到除了 emptyCtx 外,其他 ctx 自带 Mutex,是并发安全的。cancelCtx 还嵌套了一个 ctx 作为父 ctx,children map 是实现级联取消的。
cancelCtx 的 cancel 方法通过 close Done 方法返回的 <-chan struct{},所有正在监听被 cancel 的 ctx 的 Done 方法的协程全部会读到 chan 的零值,并停止阻塞并被唤醒。
这里有个非常巧妙的设计,cancel 方法是 close channel 而不是发送数据给 channel。为什么要这样设计?因为发送数据只有一个接收者,而 close channel 后所有监听 channel 的协程全部都会被唤醒,类似于广播的效果。
cancelCtx 的 children map,在父 ctx 调用了 cancel 方法后,父 ctx 会遍历 children map,调用所有子 ctx 的 cancel 方法,这样就实现了 ctx 的级联取消功能。我们可以联想成一个 ctx 树,一个 cancelCtx 被 cancel,它下面的所有子 ctx 全部被 cancel。
cancelCtx 需要 defer cancel(),把自己从父 ctx 的 children map 里移除。不 cancel,父 ctx 一直持有这个子 ctx 的引用,子 ctx 用完了也无法被 GC 回收。
timerCtx, cancel := context.WithTimeout(parentCtx, time)
timerCtx, cancel := context.WithDeadline(parentCtx, time)项目中常用这两种方法获取一个 timerCtx,区别只是 WithTimeout 方法的参数是时间段,WithDeadline 方法参数是时间戳。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}WithTimeout 只是重新调用了 WithDeadline,重新算了一遍截止时间。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}timerCtx 只是在 cancelCtx 的基础上添加了一个 timer,所以除了 cancelCtx 的级联取消,还可以实现 timer 的超时控制。
timerCtx 在创建时,会调用 time.AfterFunc(),和 timer 类似,也是 runtime 底层创建一个定时事件,只不过这次是定时执行调用了 timerCtx 的 cancel 方法。timerCtx 调用 cancel 后,Done() 返回的 channel 被 close,所有监听 ctx.Done() 的协程被唤醒,由于 timerCtx 还嵌套了 cancelCtx,所有子 ctx 也被 cancel。
和 timer 一样,AfterFunc 创建的底层定时事件也需要被 stop,所以也需要 defer cancel()。
回归到问题,给 http 请求携带一个 ctx 作为父 ctx,执行业务逻辑时的调 redis、mysql 和开启协程等每个子请求都会携带一个子 ctx 参数,上游 ctx 一旦 cancel,所有子 ctx 全部级联取消。
回头看 context 的整个设计,用 channel 传递信号、用树形结构实现级联取消,其实都是“通过通信共享内存”这个 Go 哲学的具体体现。


