Golang Context 详细原理和使用技巧
Context 背景 和 适用场景
Context 的背景
Golang 在 1.6.2 的时候还没有自己的 context,在1.7的版本中就把 golang.org/x/net/context包被加入到了官方的库中。Golang 的 Context 包,中文可以称之为“上下文”,是用来在 goroutine 协程之间进行上下文信息传递的,这些上下文信息包括 kv 数据、取消信号、超时时间、截止时间等。
Context 的功能和目的
虽然我们知道了 context 上下文的基本信息,但是想想,为何 Go 里面把 Context 单独拧出来设计呢?这就和 Go 的并发有比较大的关系,因为 Go 里面创建并发协程非常容易,但是,如果没有相关的机制去控制这些这些协程的生命周期,那么可能导致协程泛滥,也可能导致请求大量超时,协程无法退出导致协程泄漏、协程泄漏导致协程占用的资源无法释放,从而导致资源被占满等各种问题。所以,context 出现的目的就是为了解决并发协程之间父子进程的退出控制。
一个常见例子,有一个 web 服务器,来一个请求,开多个协程去处理这个请求的业务逻辑,比如,查询登录状态、获取用户信息、获取业务信息等,那么如果请求的下游协程的生命周期无法控制,那么我们的业务请求就可能会一直超时,业务服务可能会因为协程没有释放导致协程泄漏。因此,协程之间能够进行事件通知并且能控制协程的生命周期非常重要,怎么实现呢? context 就是来干这些事的。
另外,既然有大量并发协程,那么各个协程之间的一些基础数据如果想要共享,比如把每个请求链路的 tarceID 都进行传递,这样把整个链路串起来,要怎么做呢? 还是要依靠 context。
总体来说,context 的目的主要包括两个:
- 协程之间的事件通知(超时、取消)
- 协程之间的数据传递键值对的数据(kv 数据)
Context 的基本使用
Go 语言中的 Context 直接使用官方的 "context"
包就可以开始使用了,一般是在我们所有要传递的地方(函数的第一个参数)把 context.Context 类型的变量传递,并对其进行相关 API 的使用。context 常用的使用姿势包括但不限于:
- 通过 context 进行数据传递,但是这里只能传递一些通用或者基础的元数据,不要传递业务层面的数据,不是说不可以传递,是在 Go 的编码规范或者惯用法中不提倡
- 通过 context 进行协程的超时控制
- 通过 context 进行并发控制
Context 的同步控制设计
Go 里面控制并发有两种经典的方式,一种是 WaitGroup,另外一种就是 Context。
在 Go 里面,当需要进行多批次的计算任务同步,或者需要一对多的协作流程的时候;通过 Context 的关联关系(go 的 context 被设计为包含了父子关系),我们就可以控制子协程的生命周期,而其他的同步方式是无法控制其生命周期的,只能是被动阻塞等待完成或者结束。context 控制子协程的生命周期,是通过 context 的 context.WithTimeout 机制来实现的,这个是一般系统中或者底层各种框架、库的普适用法。context 对并发做一些控制包括 Context Done 取消、截止时间取消 context.WithDeadline、超时取消 context.WithTimeout 等。
比如有一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些业务逻辑,这些 goroutine 又可能会开启其他的 goro