Context 是一个请求的全局上下文,携带了截止时间、手动取消等信号,并包含一个并发安全的map用于携带数据。
主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等
一、了解Context
1 | type Context interface { |
Golang为context定义了一个接口,一个仅仅包含了四个方法的接口。从注释中我们了解到这四个方法的说明。
1.Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
2.one方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。之后,Err 方法会返回一个错误,告知为什么 Context 被取消。
3.Err方法返回取消的错误原因,因为什么Context被取消。
4.Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
二、为什么需要 Context
每一个长请求都应该有个timeout限制
- 需要在不同请求的调用中传递这个timeout
- 比如开始处理请求的时候我们说是 3 秒钟超时
- 那么在函数调用中途,这个超时还剩多少时间了?
- 需要把这个信息存储在哪里,才能让这样请求在处理中途可以停止?
三、官方实例
Golang官方封装了四个特殊的context:emptyCtx、cancelCtx、timerCtx、valueCtx结构关系如下
emptyCtx
从代码中看出这个context和他的名字一样,是一个空的context。基于emptyCtx,官方提供了两个context实例化方法。
1.Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context,它不能被取消。Background()获取。
2.TODO,如果我们不知道该使用什么Context的时候,可以使用这个,但是实际应用中,暂时还没有使用过这个TODO。TODO()获取。
但事实上我并没有理解这两个context的区别
valueCtx
1 | type valueCtx struct { |
由于它直接将Context作为匿名字段,因此仅管它只实现了2个方法,其他方法以组合的方式继承自父context。通过WithValue()方法创建,创建时需要传入父context。
1 |
|
从valueCtx的取值方式可以看出,他是一个递归的过程。当前context的key不相等的时候会从父context中查找直至最顶层的context,如果key相等则直接返回。
1 |
|
cancelCtx
1 | type cancelCtx struct { |
通过WithCancel()方法创建,创建时需要传入父context。
children中保存了所有子canceler,当外部触发cancel时,会调用children中所有的cancel()方法来终止所有的cancelCtx。cancel() 方法的功能就是关闭 channel:c.done;递归地取消它的所有子节点;
done表示是否被cancel。当外部触发cancel、或者父context的channel关闭时,done也会关闭。
WithCancel()方法获取一个cancelCtx实例, 调用WithCancel时, 首先会创建一个 cancelCtx ,并且一直向上回溯parentContext的类型,如果能找到一个cancelCtx,就将当前生成的cancelCtx设置成parentContext的Children(这样context取消的时候,才能向子节点传递,并取消各子节点)。 如果在parentContext中没有找到cancelCtx,就在本地启动一个 goroutine 用来处理 Done Channel 上的Close事件。
timerCtx
1 | type timerCtx struct { |
可以通过WithDeadline()方法创建,创建时需要传入父context及绝对时间。
可以通过WithTimeout()方法创建,创建时需要传入父context及时间间隔。WithTimeout()方法会生成一个绝对时间并调用WithDeadline()。
1 | func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { |
仍然要把子节点挂靠到父节点,一旦父节点取消了,会把取消信号向下传递到子节点,子节点随之取消。创建过程中,如果父节点context的deadline早于指定时间。直接构建一个可取消的 context。 原因是一旦父节点超时,自动调用 cancel 函数,子节点也会随之取消。所以不用单独处理子节点的计时器时间到了之后,自动调用 cancel 函数。
如果当前时间早于deadline时间则直接取消并传入error(DeadlineExceeded)。当到达deadline时timer会调用cancel()方法自动取消并传入error(DeadlineExceeded)。
四、Context使用原则
- 不要把Context放在结构体中,要以参数的方式传递,父Context一般为Background
- 应该要把Context作为第一个参数传递给入口请求和出口请求链路上的每一个函数,放在第一位,变量名建议都统一,如ctx。
- 给一个函数方法传递Context的时候,不要传递nil,否则在tarce追踪的时候,就会断了连接
- Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
- Context是线程安全的,可以放心的在多个goroutine中传递
- 可以把一个Context对象传递给任意个数的 gorotuine,对它执行取消操作时,所有goroutine都会接收到取消信号。