一、Context包简介
1.1 Context包简介
在Go语言的并发模型中,goroutine是轻量级线程的实现,它们可以用来处理并行任务。但是,随着goroutine的数量增加,管理它们的生命周期和相互之间的通信变得越来越复杂。这就是context包发挥作用的地方。
在 Go 1.7 版本中正式引入新标准库 Context包 为 goroutine 之间的信号传递、截止时间的设置、请求作用域的值传递提供了一种标准化的方式。主要在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制,除此之外,context还兼具一定的数据存储能力。
主要的作用是在 goroutine 中进行上下文的传递,而在传递信息中又包含了 goroutine 的运行控制、上下文信息传递等功能。
Context包是Go并发编程中不可或缺的一部分,它为管理goroutine的生命周期和传递请求范围的数据提供了强大的工具。
Context 应用场景
- 上层任务取消后,所有的下层任务都会被取消;中间某一层的任务取消后,只会将当前任务的下层任务取消,而不会影响上层的任务以及同级其它任务;
- 业务需要对依赖的 IO操作、数据库操作、RPC调用 或 API接口调用 有针对性的做超时控制,防止这些依赖导致服务超时;
- 为了详细了解服务性能,记录详细的调用链Log等;
Context的核心类型
context.Context 是一个接口,它定义了四个方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
type Context interface {
// 返回绑定当前ctx的任务被取消的时间 (如果没设置截止时间, 将返回 ok == false)
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that is closed when this Context is canceled or times out.
// 返回一个channel,这个Channel会在 ctx 被取消(调用cancel方法) / 到期(times out)时被关闭,
// 可以用来监听Context何时被取消。
Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel is closed.
// 如果Done返回的chan,没有关闭,err()返回nil;已经关闭,err()返回非nil,解释goroutine被取消的原因,
// 通常是 context.Canceled 或 context.DeadlineExceeded。
Err() error
// Deadline returns the time when this Context will be canceled, if any.
// Value returns the value associated with key or nil if none.
// 返回ctx存储的键值对中当前key对应的value(如果没有对应的key, 则返回nil)
Value(key interface{}) interface{}
}
|
Tips: context所包含的额外信息键值对是如何存储的呢?
其实可以想象一颗树,树的每个节点可能携带一组键值对,如果当前节点上无法找到key所对应的值,就会向上去父节点里找,直到根节点,具体后面会说到。
Context的标准error
1
2
3
4
5
6
7
8
9
|
var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }
|
- Canceled:context 被 cancel 时会报此错误;
- DeadlineExceeded:context 超时时会报此错误;
Canceler 接口,它定义了Context 的cancel函数:
1
2
3
4
|
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
|
- cancel:调用当前 context 的取消方法。
- Done:与前面一致,可用于识别当前 channel 是否已经被关闭。
Background 和 TODO 函数
从上面可以看到,Context是一个接口,想要使用它,就得实现其方法。在context包内部已经实现好了两个 emptyCtx,可以通过调用 Background()
、TODO()
方法获取。一般的将它们作为Context的根,往下派生新的context:
- context.Background(): 返回一个空的Context,这个Context既没有截止日期、不可取消、也没有key-value键值对数据,通常被用于主函数、初始化以及测试中,作为一个顶层的ctx,也就是说一般我们创建的ctx都是基于Background。
- context.TODO(): 返回一个空的Context,在不确定使用什么context的时候才会使用(context一定不能为nil,如果不确定,可以使用context.TODO()生成一个empty的context)。
WithCancel、WithDeadline、WithTimeout 函数创建子context
这三个函数比较类似,均会基于 parent Context 返回一个子 ctx 以及一个 Cancel 方法。如果调用了cancel 方法,ctx 以及基于 ctx 构造的子 context 都会被取消。
不同点在于 WithCancel 必需要手动调用 cancel 方法 做取消操作,WithDeadline 可以设置一个时间点,WithTimeout 是设置调用的持续时间,到指定时间后,会自动调用 cancel 做取消操作。
- context.WithCancel(parent Context) (ctx Context, cancel CancelFunc): 返回一个新的 Context 和 一个 CancelFunc,这个 Context 会在 父Context 的 Done关闭 或者 CancelFunc 函数被调用时 取消。
- context.WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc): 返回一个新的 Context 和 一个 CancelFunc,这个 Context 会在 父Context 的 Done关闭、CancelFunc 函数被调用 或 到达指定的截止时间时 取消。
- context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc): 返回一个新的 Context 和 一个 CancelFunc,这个 Context 会在 父Context 的 Done关闭、CancelFunc 函数被调用 或 超时时 取消。WithTimeout 是 WithDeadline的简化,内部会计算出相对于当前时间的截止日期。
CancelFunc
CancelFunc是一个函数类型,它不接受任何参数也不返回任何值。当调用WithCancel、WithDeadline 或 WithTimeout函数时,除了派生出的Context外,还会返回一个CancelFunc。调用这个CancelFunc可以取消对应的Context,以及任何从它派生出的子Context。
使用context包时,重要的是要保证CancelFunc被调用,以释放Context相关的资源。通常在defer语句中调用CancelFunc来确保这一点。
1
2
|
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 当我们不再需要这个 Context 时,确保释放资源
|
WithValue 函数创建传递 key-value对 数据的 Context
WithValue函数 是用来创建传递 traceId、token 等重要数据的 Context。
1
|
func WithValue(parent Context, key, val interface{}) Context
|
WithValue 会构造一个新的context,新的context 会包含一对 Key-Value 数据,可以通过 ctx.Value(Key)
获取存在 ctx 中的 Value 值,如果没有对应的key, 则返回nil。
1.2 Context 原理
Context 的调用是链式的,通过调用 WithCancel
、WithDeadline
、WithTimeout
或 WithValue
函数从 一个 Context 对象 派生出新的子类对象,当父 Context 被取消时,来带其派生的所有 Context 都将取消。
通过 context.WithXXX 函数都将返回新的 Context 和 CancelFunc(WithValue
仅返回 新的 Context,不返回 )。
- 调用 CancelFunc 将取消子代context,移除父代对子代的关联,并且停止所有子代定时器;
- 未能调用 CancelFunc 将泄漏子代,直到父代被取消或定时器触发。go vet工具检查所有流程控制路径上使用 CancelFuncs。
context 包中实现 Context 接口的几类 struct 结构体及其关联关系结构如下图:
1.3 Context的使用
Context 使用遵循规则
本质上 Go 语言是基于 context
来实现和搭建了各类 goroutine 控制的,并且与 select-case
联合,就可以实现进行上下文的截止时间、信号控制、信息传递等跨 goroutine 的操作,这是 Go 语言协程的重中之重。
Context 的使用遵循以下规则,以保持包之间的接口一致,并启用静态分析工具以检查上下文传播。
- 不要将 Context 放入结构体,相反 context应该作为第一个参数传入函数,通常命名为 ctx。
1
2
3
|
func DoSomething(ctx context.Context,arg Arg) error {
// ... use ctx ...
}
|
- 即使函数允许,也 不要传入 nil 的 Context。
如果不知道用哪种 Context,可以使用
context.TODO()
创建一个 context。
- context.Value方法只应该用于在程序和接口中传递和请求相关的元数据,不要用它来传递一些可选的参数。
- 相同的 Context 可以传递给不同的 goroutine,Context 是并发安全的。
正确使用context可以使得你的程序更加健壮,对于并发控制和资源管理非常有帮助。
context包提供了两个用于新建上下文的函数:Background 和 TODO。这两个函数分别用于没有任何附加信息和当前不清楚应该使用哪个Context的情况。
创建Context
使用context.WithCancel、context.WithDeadline、context.WithTimeout 和 context.WithValue这四个函数可以基于现有的Context创建子Context。
1
2
|
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 当不再需要这个context时,调用cancel释放资源
|
传递Context
将Context作为参数传递给函数是一种标准的做法,这样可以控制函数的执行。如果Context被取消,那么基于这个Context的所有goroutine都应该尽快地停止当前工作并退出
Context应该是函数的第一个参数,通常命名为ctx。
监听Context取消
通过监听Done方法返回的Channel,函数可以知道何时停止当前工作。
1
2
3
4
5
6
7
|
select {
case <-ctx.Done(): // 如果收到这个信号,表示上级调用已经取消了这个 Context
return ctx.Err()
default: // 执行正常的业务逻辑
// TODO: worker
}
|
示例:
1
2
3
4
5
6
7
8
9
10
11
12
|
func main() {
parentCtx := context.Background()
ctx, cancel := context.WithTimeout(parentCtx, 1*time.Millisecond)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
|
通过调用标准库 context.WithTimeout
方法针对 parentCtx 变量设置了超时时间,并在随后调用 select-case
进行 context.Done
方法的监听,最后由于达到截止时间运行到了。因此逻辑上 select 最后走到了 context.Err 的 case <-ctx.Done()
分支,最终输出 context deadline exceeded。
Tips: golang 中的 select 就是用来监听和 channel 有关的 IO 操作,当 IO 操作发生时,触发相应的动作,select 只能应用于 channel 的操作,既可以用于 channel 的数据接收,也可以用于 channel 的数据发送:
- 如果 select 的多个分支都满足条件,则会随机的选取其中一个满足条件的分支执行;
- 如果所有条件都不满足,并且有 default 子句,则执行 default 子句,否则会阻塞,直至有一个 case 产生 IO 操作;
二、Context 的实现
2.1 Context核心数据结构
Context为 interface 类型,定义了4个核心api:
1
2
3
4
5
6
|
type Context interface {
Deadline() (deadline time.Time, ok bool) // 返回context的过期时间
Done() <-chan struct{} // 返回context中的chan,当times out 或 调用cancel方法时,返回的c是close掉的 chan
Err() error // 返回错误,该context为什么被取消掉。
Value(key any) any // 返回context中存储对应key的值
}
|
- Deadline():返回绑定当前ctx的任务超时时间(如果没设置截止时间, 将返回 ok == false)
- Done(): 当ctx被取消/到期时,返回一个关闭的chan (如果当前ctx不会被取消, 将返回nil)
- Err():Done返回的chan,如果没有关闭,err()返回nil;如果已经关闭,err()返回非nil,解释说明goroutine被取消的原因
- Value():返回ctx存储的键值对中当前key对应的value(如果没有对应的key, 则返回nil)
Tips: context所存储的键值对信息,其实可以想象一颗树,树的每个节点可能携带一组键值对,如果当前节点上无法找到key所对应的值,就会向上去父节点里找,直到根节点。
Context的标准error:
1
2
3
4
5
6
7
8
9
|
var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }
|
- Canceled:context 被 cancel 时会报此错误;
- DeadlineExceeded:context 超时时会报此错误;
在context包内定义并了 emptyCtx 的上下文结构,并已经实现好了两个emptyCtx 对象,可以通过调用 Background()、TODO() 方法获取。一般的将它们作为Context的根,往下派生新的Context。
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
37
38
39
40
41
42
43
44
45
46
47
48
49
|
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key any) any {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
|
- Background():通常被用于主函数、初始化以及测试中,作为一个顶层的ctx,也就是说一般创建的ctx都是基于Background;
- TODO()**:在不确定使用什么context的时候才会使用(context一定不能为nil,如果不确定,可以使用context.TODO()生成一个empty的context)
2.2 Context 的 WithXXX系列函数:创建子Ctx
- WithXXX 的各种方法比较类似,均会基于 parent Context 生成一个子 ctx,以及一个 Cancel 方法。如果调用了cancel 方法,ctx 以及基于 ctx 构造的子 context 都会被取消。不同点在于 WithCancel 必需要手动调用 cancel 方法,WithDeadline 可以设置一个时间点,WithTimeout 是设置调用的持续时间,到指定时间后,会自动调用 cancel 做取消操作。
- 除了上面的构造方式,还有一个
WithValue
函数 是用来创建传递 key-value对 重要数据的 Context,它会构造一个新的context,新的context 会包含一对 Key-Value 数据,可以通过Context.Value(Key) 获取存在 ctx 中的 Value 值。
2.2.1 WithCancel – cancelCtx
WithCancel 函数创建的 Context 必须手动调用cancle()才能取消。
在调用 context.WithCancel 方法时,我们会涉及到 cancelCtx 类型,其主要特性是取消事件。源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
/*
* 给parent ctx新建一个子节点(类型cancleCtx)
* @return ctx: 新子节点 cacle: 取消函数
*/
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent) // 新建cancleCtx(详细见4.4.)
propagateCancel(parent, &c) // 来建立当前节点与祖先节点这个取消关联逻辑
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
|
其中的 newCancelCtx 方法将会生成出一个可以取消的新 context,如果该 context 执行取消,与其相关联的子 context 以及对应的 goroutine 也会收到取消信息。
首先 main goroutine 创建并传递了一个新的 context 给 goroutine a,此时 goroutine a 的 context 是 main goroutine context 的子集,以此类推:
传递过程中,goroutine a 再将其 context 一个个传递给了 goroutine c、d、e。最后在运行时 goroutine a 调用了 cancel 方法。使得该 context 以及其对应的子集均接受到取消信号,对应的 goroutine a、c、d、e 也进行了响应。
2.2.2 WithDeadline – timerCtx
- 手动调用cancle()能取消;
- 超时后,会自动调用cancle()取消ctx;
1
2
3
4
5
6
|
/*
* 给parent ctx新建一个子节点(带超时, 类型cancleCtx)
* @param [in] deadline 过期时间点
* @return ctx: 新子节点 cacle: 取消函数
*/
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
|
WithDeadline 的最后期限调整为不晚于 deadline 返回父上下文的副本。如果父母的截止日期已经早于 deadline,WithDeadline (父,d) 是在语义上等效为父。返回的上下文完成的通道关闭的最后期限期满后,返回的取消函数调用时,或当父上下文完成的通道关闭,以先发生者为准。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 创建son ctx, 到期时间设置为50ms
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(50 * time.Millisecond))
//即使ctx将过期,在任何情况下调用它的cancel函数都是一个好习惯。
//如果不这样做,可能会使ctx及其父节点存活的时间超过必要时间。
defer cancel()
select {
case <-time.After(100 * time.Millisecond): // parent ctx, 到期时间为100ms
fmt.Printf("parent ctx is finish: overslept\n")
case <-ctx.Done(): // son ctx先到期(50ms), 到期后会向ctx.Done()写入数据 ==> 因此执行下面的语句
fmt.Printf("son ctx is finish, err:%s\n", ctx.Err())
}
}
|
2.2.3 WithTimeout – timerCtx
- 手动调用cancle()能取消;
- 超时后,会自动调用cancle()取消ctx;
1
2
3
4
5
6
7
|
/*
* 给parent ctx新建一个子节点(带超时, 类型cancleCtx)
* @param [in] deadline 接收一个相对当前时间的过期时长timeout
* 等待于 WithDeadline(parent, time.Now().Add(timeout))
* @return ctx: 新子节点 cacle: 取消函数
*/
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
|
2.2.4 WithValue – valueCtx
这里添加键值对不是在原parent context结构体上直接添加,而是以此parent context作为父节点,重新创建一个新的valueCtx子节点,将键值对添加在子节点上,由此形成一条context链。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 为parent创建新的子ctx,该ctx携带<key,val>键值对信息
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil { // 断言:parent不为nil
panic("cannot create context from nil parent")
}
if key == nil { // 断言:key不为nil
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() { // 断言:key可比较
panic("key is not comparable")
}
// 以parent为父亲,创建新的子节点valueCtx,包含<key,val>
return &valueCtx{parent, key, val}
}
|
2.3 Context的派生类
在使用场景中可以看到context包本身包含了数个导出函数,包括WithValue、WithTimeout等,无论是最初构造context还是传导context,最核心的接口类型都是context.Context,任何一种context也都实现了该接口,包括value context。
2.3.1 emptyCtx派生类
- emptyCtx没有超时时间,不能取消,也不能携带任何额外信息
- emptyCtx用来作为context树的根节点
- 一般不会直接使用emptyCtx,而是使用由emptyCtx实例化的两个变量,分别可以通过调用Background和TODO方法得到。(区别见4.2.1)
在context包内定义并了 emptyCtx 的上下文结构,并已经实现好了两个emptyCtx 对象,可以通过调用 Background()、TODO() 方法获取。一般的将它们作为Context的根,往下派生新的Context。
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
37
38
39
40
41
42
43
44
45
46
47
48
49
|
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key any) any {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
|
- Background():通常被用于主函数、初始化以及测试中,作为一个顶层的ctx,也就是说一般创建的ctx都是基于Background;
- TODO()**:在不确定使用什么context的时候才会使用(context一定不能为nil,如果不确定,可以使用context.TODO()生成一个empty的context)
2.3.2 cancelCtx派生类
继承自Context,补充了新的功能:
- 实现了 canceler接口,支持取消当前ctx下所有的子ctx, 必须手工取消
1
2
3
4
5
6
7
8
9
10
11
12
13
|
type cancelCtx struct {
Context // 继承了Context接口,表示valueCtx所属的父节点
mu sync.Mutex // 保护下面的字段
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call, chan(用来传递关闭信号): 惰性创建
children map[canceler]struct{} // 存储当前ctx节点下所有的子节点
err error // 存储错误信息,表示人物结束的原因
}
// cancleer接口
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
|
该结构体所包含的属性也比较简单,主要是 children 字段,其包含了该 context 对应的所有子集 context,便于在后续发生取消事件的时候进行逐一通知和关联。
done 属性(只读 channel)是在真正调用到 Done 方法时才会去创建。需要配合 select-case 来使用。
cancelCtx 除了实现Context接口,还实现了 canceler 接口,Done()和cancle()详细见下:
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
37
38
39
40
41
|
// 创建done通道, 用于通信
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
// 如果c.done不存在, 就创建一个
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done // 保存到变量d
c.mu.Unlock()
return d // 返回d
}
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
// 设置取消原因
c.err = err
if c.done == nil { // 设置一个关闭的channel
c.done = closedchan
} else { // 将c.done通道关闭,用以发送关闭信号
close(c.done)
}
// 将子节点context依次取消
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c) // 将当前context节点从父节点上移除
}
}
|
2.3.3 timerCtx派生类
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
|
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 内部使用cancelCtx实现取消
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
// 取消计时器
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
|
2.3.4 valueCtx派生类
在调用 context.WithValue 方法时,会涉及到 valueCtx 类型,其主要特性是涉及上下文信息传递,继承自Context,添加了成员属性:key,val,能够携带额外的信息,源码如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
type valueCtx struct {
Context // 继承了Context接口,表示valueCtx所属的父节点
key, val interface{} // 携带key-value键值对,同来保存额外信息
}
// 实现了Value方法:用来在context链上(直到根节点),寻找key对应的value
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key { /* 当前c上找到, 就返回val */
return c.val
}
/* 在c所属的父节点c.Context上继续调用Value, 查找key对应的val */
return c.Context.Value(key)
}
|
本质上 valueCtx 类型是一个单向链表,会在调用 Value 方法时先查询自己的节点是否有该值。若无,则会通过自身存储的上层父级节点的信息一层层向上寻找对应的值,直到找到为止。
而在实际的工程应用中,你会发现各大框架,例如:gin、grpc 等。他都是有自己再实现一套上下文信息的传输的二次封装,本意也是为了更好的管理和观察上下文信息。
2.4 context 如何实现跨 goroutine 的取消事件并传播开来
context 能实现跨 goroutine 的取消事件并传播开来,是在于 WithCancel
和 WithDeadline
都会调用到的 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
37
38
39
40
41
42
43
44
45
46
47
48
|
// src/context/context.go
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
// 当父级上下文(parent)的 Done 结果为 nil 时,将会直接返回,
// 因为其不会具备取消事件的基本条件,可能该 context 是 Background、TODO 等方法产生的空白 context。
done := parent.Done()
if done == nil {
return // parent is never canceled
}
// 当父级上下文(parent)的 Done 结果不为 nil 时,继续往下走
select {
case <-done: // 发现父级上下文已经被取消,作为其子级,该 context 将会触发取消事件并返回父级上下文的取消原因。
// parent is already canceled
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
// 父级 context 未触发取消事件,当前父级和子级 context 均正常(未取消),继续往下走
// 调用 parentCancelCtx 方法找到具备取消功能的父级 context。
// 并将当前 context,也就是 child 加入到 父级 context 的 children 列表中,
// 等待后续父级 context 的取消事件通知和响应。
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err, p.cause)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// 如果调用 parentCancelCtx 方法没有找到具备取消功能的父级 context,
// 将会启动一个新的 goroutine 去监听父子 context 的取消事件通知。
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
}
|
通过对 context 的取消事件和整体源码分析,可得知 cancelCtx 类型的上下文包含了其下属的所有子节点信息:
也就是其在 children 属性的 map[canceler]struct{} 存储结构上就已经支持了子级关系的查找,也就自然可以进行取消事件传播了。
而具体的取消事件的实际行为,则是在前面提到的 propagateCancel 方法中,会在执行例如 cacenl 方法时,会对父子级上下文分别进行状态判断,若满足则进行取消事件,并传播给子级同步取消。
三、Context使用技巧
3.1 构造Context
一般来说,使用 ctx := context.Background()
作为Context的根,往下派生。
如果拿捏不准是否需要一个全局的context,可以使用下面这个函数构造:ctx := context.TODO()
作为Context的根,往下派生。
3.2 Context传值方式
阅读 contxt 包的源码可以看出,valueCtx 对象(调用 WithValue
函数创建)不适合做为存储介质来存放大量的 k-v 数据,原因如下:
- 一个 valueCtx 实例对象只能存一个 k-v 对,因此 n 个 k-v 对会嵌套 n 个 valueCtx,造成空间浪费;
- 基于 k 寻找 v 的过程是线性的,时间复杂度 O(N);
- 不支持基于 k 的去重,相同 k 可能重复存在,并基于起点的不同,相同 k 返回的 v 可能不同;
context 包中 valueCtx 的定位类似于请求头,存储的应该是一些共同的数据。例如:登陆的 session、cookie 等信息;
Context传值方式:
- 不能使用传引用方式,而是使用传值方式
- 只能自顶向下传值,反之则不可以
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package main
import (
"context"
"fmt"
)
func func1(ctx context.Context) {
// WithValue创建:携带(k1,v1)的子节点ctx
ctx = context.WithValue(ctx, "k1", "v1")
// 获取ctx的key="k1"对应的val值
func2(ctx)
}
func func2(ctx context.Context) {
// Value获取
fmt.Println(ctx.Value("k1").(string))
}
func main() {
ctx := context.Background()
func1(ctx)
}
// 执行结果: v1
|
3.3 取消cancel
如果有cancel,一定要保证调用,否则会造成资源泄露,比如timer泄露。
- cancel函数是幂等的,可以被多次调用
- context中包含done channel可以用来确认是否取消、通知取消;