上下文Context
上下文 Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。上下文与 Goroutine 有比较密切的关系。
context.Context接口
需要实现四个方法
1 | type Context interface { |
Deadline
- 返回 context.Context 被取消的时间,也就是完成工作的截止日期,如果未设置截止日期,则返回ok==false;
Done
- 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,如果context永远无法取消(?)则可能返回nil,多次调用 Done 方法会返回同一个 Channel;channel的关闭可能会异步发生
Err
返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;
如果 context.Context 被取消,会返回 Canceled 错误;
如果 context.Context 超时,会返回 DeadlineExceeded 错误;
Value
- 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据; ### 设计原理
Context的相关操作
默认上下文
context包中最常用的是context.Background context.TODO,两个方法会返回预先初始化好的私有变量
1 | var ( |
1 | type emptyCtx int |
emptyCtx永远不会取消,没有值,也没有截止日期。它不是struct {},因为此类型的变量必须具有不同的地址。
emptyCtx通过空方法实现了context.Context接口的所有方法,他没有任何功能
Background和TODO都会返回一个emptyCtx,不同之处在于(其实只是语义上略有不同,实际上二者互为别名)
- Background通常由main方法,初始化和单元测试使用,并且用作传入请求(request)的顶级上下文
- TODO主要用于当不清楚要使用哪个Context或尚不可用时(因为尚未扩展周围的函数以接受Context参数),
取消信号
context.WithCancel
函数能够从 context.Context
中衍生出一个新的子上下文并返回用于取消该上下文的函数。一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。
1 | func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { |
WithCancel方法通过newCancelCtx返回一个cancelCtx结构体
propagateCancel方法:会构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
上述函数总共与父上下文相关的三种不同的情况:
- 当
parent.Done() == nil
,也就是parent
不会触发取消事件时,当前函数会直接返回; - 当child的继承链包含可以取消的上下文时,会判断parent是否已经触发了取消信号;
- 如果已经被取消,
child
会立刻被取消; - 如果没有被取消,
child
会被加入parent
的children
列表中,等待parent
释放取消信号;
- 如果已经被取消,
- 当父上下文是开发者自定义的类型、实现了
context.Context
接口并在Done()方法中返回了非空的管道时;- 运行一个新的 Goroutine 同时监听
parent.Done()
和child.Done()
两个 Channel; - 在
parent.Done()
关闭时调用child.cancel
取消子上下文;
- 运行一个新的 Goroutine 同时监听
cancelCtx:
1 | type cancelCtx struct { |
canceler是可以直接取消的Context类型,canceler接口:
1 | type canceler interface { |
cancelCtx本身也继承了canceler接口和context.Context接口:
1 | func (c *cancelCtx) Value(key interface{}) interface{} { |
在cancel方法里
关闭cancelCtx的done channel
给cancelCtx的err赋值
遍历cancelCtx的children(子Ctx)并链式调用其cancel方法
若removeFromParent为true则代表当前Ctx存在父级Ctx,则从父级Ctx的children中删除当前Ctx:
1
2
3
4
5
6
7
8
9
10
11func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
除了 context.WithCancel 之外,context 包中的另外两个函数 context.WithDeadline 和 context.WithTimeout 也都能创建可以被取消的计时器上下文 context.timerCtx
1 | func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { |
WithDeadline方法通过比较父context的截止时间和当前context的截止时间,算出context存活时间,通过调用time.AfterFunc匿名函数定时取消context
传值方法
父context调用WithValue方法可以创建一个子上下文用来传值,子context的类型为valueCtx:
1 | type valueCtx struct { |
valueCtx继承了Context,另携带了一个键值对,重写了Value方法:
1 | func (c *valueCtx) Value(key interface{}) interface{} { |
如果传入的键与context中存储的不相符则从父上下文中查找该键对应的值,直到某个父context返回nil或者查找到对应的值