Golang 12_Golang上下文Context

一、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 的调用是链式的,通过调用 WithCancelWithDeadlineWithTimeoutWithValue 函数从 一个 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 的使用遵循以下规则,以保持包之间的接口一致,并启用静态分析工具以检查上下文传播。

  1. 不要将 Context 放入结构体,相反 context应该作为第一个参数传入函数,通常命名为 ctx
1
2
3
func DoSomething(ctx context.Contextarg Arg) error { 
    // ... use ctx ... 
}
  1. 即使函数允许,也 不要传入 nil 的 Context。 如果不知道用哪种 Context,可以使用 context.TODO() 创建一个 context。
  2. context.Value方法只应该用于在程序和接口中传递和请求相关的元数据,不要用它来传递一些可选的参数。
  3. 相同的 Context 可以传递给不同的 goroutine,Context 是并发安全的。

正确使用context可以使得你的程序更加健壮,对于并发控制和资源管理非常有帮助。

context包提供了两个用于新建上下文的函数:BackgroundTODO。这两个函数分别用于没有任何附加信息和当前不清楚应该使用哪个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 的取消事件并传播开来,是在于 WithCancelWithDeadline 都会调用到的 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可以用来确认是否取消、通知取消;