前几篇中提到 等待多个 goroutine 协作的方式可以使用WaitGroup. 但是有一种场景我们无论是使用Mutex, sync/Once,都无法满足。
场景如下
现在有一个 Server 服务在执行,当请求来的时候我们启动一个 goroutine 去处理,然后在这个 goroutine 当中有对下游服务的 rpc 调用,也会去请求数据库获取一些数据,这时候如果下游依赖的服务比较慢,但是又没挂,只是很慢,可能一次调用要 1min 才能返回结果,这个时候我们该如何处理?
如下图所示, 首先假设我们使用WaitGroup进行控制, 等待所有的goroutine处理完成之后返回,可以看到我们实际的好事远远大于了用户可以容忍的时间
如下图所示,再考虑一个常见的场景,万一上面的 rpc goroutine 很早就报错了,但是 下面的 db goroutine 又执行了很久,我们最后要返回错误信息,很明显后面 db goroutine 执行的这段时间都是在白白的浪费用户的时间。
这时候就应该请出context包了, context主要就是用来在多个 goroutine中设置截至日期, 同步信号, 传递请求相关值。
每一次 context 都会从顶层一层一层的传递到下面一层的 goroutine 当上面的 context 取消的时候,下面所有的 context 也会随之取消
上面的例子当中,如果引入 context 后就会是这样,如下图所示,context 会类似一个树状结构一样依附在每个 goroutine 上,当上层的 req goroutine 的 context 超时之后就会将取消信号同步到下面的所有 goroutine 上一起返回,从而达到超时控制的作用
如下图所示,当 rpc 调用失败之后,会出发 context 取消,然后这个取消信号就会同步到其他的 goroutine 当中
1.2 还有一种场景Golang context是Golang应用开发常用的并发控制技术,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine。
context翻译成中文是 上下文,即它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。
典型的使用场景如图所示
上图中由于goroutine派生出子goroutine,而子goroutine又继续派生新的goroutine,这种情况下使用WaitGroup就不太容易,因为子goroutine个数不容易确定。
二. context 2.1 使用说明 2.1.1 使用准则下面几条准则
对 server 应用而言,传入的请求应该创建一个 context
通过 WithCancel , WithDeadline , WithTimeout 创建的 Context 会同时返回一个 cancel 方法,这个方法必须要被执行,不然会导致 context 泄漏,这个可以通过执行 go vet 命令进行检查
应该将 context.Context 作为函数的第一个参数进行传递,参数命名一般为 ctx 不应该将 Context 作为字段放在结构体中。
不要给 context 传递 nil,如果你不知道应该传什么的时候就传递 context.TODO()
不要将函数的可选参数放在 context 当中,context 中一般只放一些全局通用的 metadata 数据,例如 tracing id 等等
context 是并发安全的可以在多个 goroutine 中并发调用
2.1.2 函数签名context 包暴露的方法不多,看下方说明即可
// 创建一个带有新的 Done channel 的 context,并且返回一个取消的方法 func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // 创建一个具有截止时间的 context // 截止时间是 d 和 parent(如果有截止时间的话) 的截止时间中更早的那一个 // 当 parent 执行完毕,或 cancel 被调用 或者 截止时间到了的时候,这个 context done 掉 func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) // 其实就是调用的 WithDeadline func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) type CancelFunc type Context interface // 一般用于创建 root context,这个 context 永远也不会被取消,或者是 done func Background() Context // 底层和 Background 一致,但是含义不同,当不清楚用什么的时候或者是还没准备好的时候可以用它 func TODO() Context // 为 context 附加值 // key 应该具有可比性,一般不应该是 string int 这种默认类型,应该自己创建一个类型 // 避免出现冲突,一般 key 不应该导出,如果要导出的话应该是一个接口或者是指针 func WithValue(parent Context, key, val interface{}) Context 2.2. 源码 2.2.1 context.Context接口 type Context interface { // 返回当前 context 的结束时间,如果 ok = false 说明当前 context 没有设置结束时间 Deadline() (deadline time.Time, ok bool) // 返回一个 channel,用于判断 context 是否结束,多次调用同一个 context done 方法会返回相同的 channel Done() <-chan struct{} // 当 context 结束时才会返回错误,有两种情况 // context 被主动调用 cancel 方法取消:Canceled // context 超时取消: DeadlineExceeded Err() error // 用于返回 context 中保存的值, 如何查找,这个后面会讲到 Value(key interface{}) interface{} } 2.2.2 context.Backgroud