Go Context源码阅读
简介
context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、元数据传递等。 本文通过对Context源码阅读,对其有更深的理解,以便在工作中正确使用。
使用场景
context
是Go
开发常用的并发控制,常用在异步场景中用于实现并发协调以及对 goroutine
的生命周期控制,与WaitGroup
最大的不同点是context
对于父子之间goroutine
有更强的控制力,它可以控制多级的goroutine
。
context
主要用来在 goroutine
之间传递上下文信息,包括:取消信号、超时时间、截止时间、元数据传递等。
在Go
里,不能直接杀死协程,协程的关闭一般通过 channel+select
方式来控制。因此在一些在多级goroutine
的情况下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline
等,而且可以同时被关闭。再用 channel+select
就会比较麻烦,这时就可以通过 context
来实现。
Context代码阅读
代码路径 /src/context/context.go
代码版本 GOVERSION='go1.21.1'
Context接口定义
type Context interface {
// 返回 context 过期时间,只有定时取消才会用到该函数
Deadline() (deadline time.Time, ok bool)
// 返回 context 中的 channel;
Done() <-chan struct{}
// 返回错误
Err() error
// 返回 context 中的对应 key 的值
Value(key any) any
}
-
Deadline()
:返回一个deadline
和标识是否已设置deadline
的bool
值,如果没有设置deadline
,则ok == false
,此时deadline为一个初始值的time.Time
值。 -
Done()
:该方法返回一个channel
,需要在select-case
语句中使用,如case <-context.Done()
:- 当
context
关闭后,Done()
返回一个被关闭的管道,关闭的管道仍然是可读的,据此goroutine
可以收到关闭请求; - 当
context
还未关闭时,Done()
返回nil
。
- 当
-
Err()
:该方法描述context
关闭的原因。关闭原因由context
实现控制,不需要用户设置。比如Deadline context
,关闭原因可能是因为deadline
,也可能提前被主动关闭,那么关闭原因就会不同:- 当
context
关闭后,Err()
返回context
的关闭原因;deadline
超时关闭:"context deadline exceeded"
;cancel
主动关闭:"context canceled"
。- 当
context
还未关闭时,Err()
返回nil
;
- 当
-
Value()
:类型是valueCtx
的context
,它不是用于控制呈树状分布的goroutine
,而是用于在树状分布的goroutine
间传递信息。Value()
方法就是用于此种类型的context
,该方法根据key
值查询map
中的value
。
emptyCtx 结构体
主要用于context
的根节点,空的context
只是简单的实现了Context
,本身不包含任何值,仅用于其他context
的父节点。
// emptyCtx 是一个空的结构体, 不能被取消,没有deadline, 没有值。 是`backgroundCtx` 和 `todoCtx`的基类
type emptyCtx struct{}
// Deadline 方法会返回一个公元元年时间以及 false 的 flag,标识当前 context 不存在过期时间;
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
//Done 方法返回一个类型只读的 nil 值,用户无论往 nil 中写入或者读取数据,均会陷入阻塞;
func (emptyCtx) Done() <-chan struct{} {
return nil
}
// Err 方法返回的错误永远为 nil;
func (emptyCtx) Err() error {
return nil
}
//Value 方法返回的 value 同样永远为 nil.
func (emptyCtx) Value(key any) any {
return nil
}
// String() 方法来定制类型的字符串形式的输出,换句话说:一种可阅读性和打印性的输出。
// 继承于emptyCtx
type backgroundCtx struct{ emptyCtx }
func (backgroundCtx) String() string {
return "context.Background"
}
type todoCtx struct{ emptyCtx }
func (todoCtx) String() string {
return "context.TODO"
}
emptyCtx
是实现了 Context
接口最简单的结构体,里面没有任何处理逻辑。
结构体目的只是为了构建Background()
和TODO()
两个函数.(见下源码)
-
Background
函数主要用来作为创建其他Context
的父Context
-
TODO
当调用函数需要Context
但是暂时不知道传入什么类型的Context
的时候,使用该函数(可以理解为临时占位用)
// 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 backgroundCtx{}
}
// 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 todoCtx{}
}
小结: context.Background()
和 ctxTodo:=context.TODO()
方法的初始化部分已经完成。
cancelCtx 结构体
使用例子
讲解代码之前,首先了解一段如何使用context.WithCancel(context.Background())
func main() {
// 创建扫描器
ticker := time.NewTicker(1 * time.Second)
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(2 * time.Second)
// 经过2s强制终止
cancel()
}()
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err().Error())
return
case <-ticker.C: // 间隔1s发送数据
fmt.Println("发送数据")
}
}
}
通过简单的编码,就可以轻松的监听取消事件的发生,简化了编程的复杂性。
cancelCtx
结构体是实现Context
接口较复杂也是最重要的一个结构体。
canceler 接口定义
源码如下:
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
实现了上面定义的两个方法的 Context```,就表明该 ``Context
是可取消的。源码中有两个类型实现了 canceler
接口:cancelCtx
和 timerCtx
。
cancelCtx 数据结构
// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context //嵌入式接口类型,cancelCtx 必然为某个context的子context;
mu sync.Mutex // 互斥锁,保护以下字段不受并发访问的影响
done atomic.Value // 原子通道,第一次调用取消函数时被惰性创建,在该context及其后代context都被取消时关闭
children map[canceler]struct{} // 值为struct{}其实是一个set,保存当前上下文的所有子上下文,第一次取消调用时设为nil
err error // 第一次取消操作时设置为一个错误值,对此上下文及后代上下文进行取消操作返回该错误
cause error // go高版本中支持设置取消原因
}
Done() 方法
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load() //基于 atomic 包,读取 cancelCtx 中的 chan;倘若已存在,则直接返回;
if d != nil {
return d.(chan struct{})
}
c.mu.Lock() //加锁后,再次检查 chan 是否存在,若存在则返回;(double check)
defer c.mu.Unlock()
d = c.done.Load()
if d == nil { // 如果不存在,创建strct{} 类型channel
d = make(chan struct{})
c.done.Store(d) // 初始化 chan 存储到 aotmic.Value 当中,并返回.(懒加载机制)
}
return d.(chan struct{})
}
c.done
使用了惰性加载(lazy loading)的机制,只有一次调用 Done()
方法的时候才会被创建,且通过加锁二次检查,确保在多个goroutine
同时调用 Done()
方法时,只有第一个goroutine
创建通道,其他goroutine
均复用已创建的通道。
函数返回的是一个只读的 channel
,一般通过搭配 select
来使用,当channel
关闭后,就会立即读出零值,据此可以判断cancelCtx
是否被取消。
这块看源代码, 大白话讲一下:
Done()
只有在被执行的时候才会去创建生成一个 只读的channel
,外部使用到 case <-ctx.Done()
: 时候会被阻塞起来。 如果上下文中执行了 cancel()
后那么直接 close()
此channel
。
如果 receive 值 从一个非 nil 但是已关闭的channel 永不阻塞。
Err() 方法
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
cancelCtx.err
默认是nil
,在context被cancel时指定一个error变量: "context canceled"
。
Valuel()方法
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey { //如果 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针;
return c
}
return value(c.Context, key) //否则 遵循 valueCtx 的思路取值返回
}
如果 key
特定值 &cancelCtxKey
,则返回 cancelCtx
自身的指针(基于 cancelCtxKey
为 key 取值时返回 cancelCtx
自身,是 cancelCtx
特有的协议)
简单讲就是:既然你要创建一个可取消的Context
,那么什么时候取消这个Context
呢?
-
要么外部主动调用
cancel
方法取消 -
要么就是被动的取消。创建
c := newCancelCtx(parent)
的时候有个父Context
,如果父Context
被取消了,那么自己作为儿子也就被取消吧
context.WithCancel()方法
context.WithCancel()
方法 是Go语言中的context
包提供的函数之一,用于创建一个可取消的上下文对象。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
func withCancel(parent Context) *cancelCtx {
if parent == nil { //校验参数parent——父context 非空;
panic("cannot create context from nil parent")
}
c := &cancelCtx{} // 创建cancelCtx对象
c.propagateCancel(parent, c) //propagateCancel方法内启动一个守护协程,当父context终止时,该cancelCtx 也被终止;
return c
}
// propagateCancel arranges for child to be canceled when parent is.
// It sets the parent context of cancelCtx.
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
// parent 父协程,child 当前协程。
c.Context = parent // 注入parent——父context构造cancelCtx;
done := parent.Done()
if done == nil { // parent 是不会被 cancel 的类型(如 emptyCtx),则直接返回。 对于cannel类型一定是不为空的
return // parent is never canceled
}
select {
case <-done:
// parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err
child.cancel(false, parent.Err(), Cause(parent))
return
default:
}
//parentCancelCtx通过 parent.Value(&cancelCtxKey) 判断 父节点是否是cancelCtx 类型
//若以特定的 cancelCtxKey 从 parent 中通过parent.Value()取值,取得的 value 是 parent 本身,则返回 true.
//若parent 的 channel 已关闭或者是不会被 cancel 的类型,则返回 false;
if p, ok := parentCancelCtx(parent); ok { // 父亲节点可以被关闭,则向上关联
// parent is a *cancelCtx, or derives from one.
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()
return
}
// 判断是否为 afterFuncer 类型的context, 后边会介绍。 go 21 中新特性
if a, ok := parent.(afterFuncer); ok {
// parent implements an AfterFunc method.
c.mu.Lock()
stop := a.AfterFunc(func() {
child.cancel(false, parent.Err(), Cause(parent))
})
c.Context = stopCtx{
Context: parent,
stop: stop,
}
c.mu.Unlock()
return
}
goroutines.Add(1) // 实际生产中没有什么用,测试用的
go func() { // 创建新的监听
select {
case <-parent.Done(): // 父节点退出
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done(): // 自己写退出
}
}()
}
propagateCancel
用以传递父子 context
之间的 cancel
事件,向上寻找可以“挂靠”的“可取消”的 context
,并且“挂靠”上去。这样,调用上层 cancel
方法的时候,就可以层层传递,将那些挂靠的子 context
同时“取消”。
当通过WithCancel(parent Context)
创建一个新的cancelContext时propagateCancel
被调用,用来确保当parent
父context
终止时,该cancelCtx
也被终止:
-
若 parent 是不会被 cancel 的类型(如
emptyCtx
),不需要传递,则直接返回; -
若 parent 已经被 cancel,则直接终止子
context
,并以 parent 的 err 作为子context
的 err; -
判断父节点为
afterFuncer
类型,则修改自己的Ctx,将以父节点 和 stop(将自己的cannel注入进去)形成新的上下文。这样父节点在调用前置逻辑后就可以调用自己的cannel(函数) -
若 parent 是
cancelCtx
的类型,则加锁,并将子 context 添加到 parent 的 children map 当中; -
若 parent 不是
cancelCtx
类型,但又存在 cancel 的能力(比如用户自定义实现的 context),则启动一个协程,通过多路复用的方式监控 parent 状态,倘若其终止,则同时终止子 context,并传递 parent 的 err
cancelCtx.cancel()
// removeFromParent bool,表示当前 context 是否需要从父 context 的 children set 中删除;
// 若当前的cancel是由父节点取消引起的,由于父节点已取消,则removeFromParent可以为false
// 第二个 err 则是 cancel 后需要展示的错误;
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
//校验传入的 err 是否为空,若为空则 panic
if err == nil {
panic("context: internal error: missing cancel error")
}
// 如果case为空,则将error赋值给case
if cause == nil {
cause = err
}
c.mu.Lock()
//校验 cancelCtx 自带的 err 是否已经非空,若非空说明已被 cancel,则解锁返回
if c.err != nil { // 这里就是为什么能频繁的 cannel的原因
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil {
//若 channel 此前未初始化,则直接注入一个 closedChan,否则关闭该 channel;
c.done.Store(closedchan)
} else {
close(d)
}
//遍历当前 cancelCtx 的 children set,依次将 children context 都进行 cancel;
for child := range c.children {
// 此时child.cancel第一个参数为false,因为父context已经关闭,会将child从set中删除
// 即若当前的cancel是由父节点取消引起的,则removeFromParent 则可以为false
child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()
// 根据传入的 removeFromParent flag 判断是否需要手动把 cancelCtx 从 parent 的 children set 中移除.
if removeFromParent {
//如果 parent 不是 cancelCtx,直接返回(因为只有 cancelCtx 才有 children set)
//加锁;从 parent 的 children set 中delete删除对应 child解锁返回.
removeChild(c.Context, c)
}
}
context.WithCancel()
函数返回一个新的上下文(context
)以及一个可用于取消该上下文的取消函数c.cancel(true, Canceled)
。
当调用cancel()取消函数时,将通过 context
的取消信号来通知这个上下文相关联的所有操作停止执行并释放资源。步骤如下:
-
判断err参数是否为空,若为空则引发
panic("context: internal error: missing cancel error")
,否则首先加锁 -
判断
cancelCtx
自带的 err 是否已经非空,若非空说明cancelCtx
已被 cance,则解锁返回。 -
判断
channel
是否初始化,若未初始化,则直接注入一个closedChan
关闭的通道,否则关闭已有的channel -
将所有的子
contex
一起取消,并解锁 -
加锁,从
parent
的children set
中delete
删除所有child
,解锁返回.
context.WithCancelCause()方法
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
// 使用方法
ctx, cancelFunc := context.WithCancelCause(parentCtx)
defer cancelFunc(errors.New("定义原因"))
可以设置额外的取消原因,也就是 error 信息
timerCtx
timerCtx结构
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx
是继承于 cancelCtx
,除了继承 cancelCtx
的能力之外,新增了一个 time.Timer
用于定时终止 context
;另外新增了一个 deadline
字段用于字段 timerCtx
的过期时间.
timerCtx.Deadline()方法
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
由于继承了 cancelCtx
结构体,timerCtx
可以从其父亲结构体获得取消能力,同时也可以使用它的成员变量 timer
来设置超时。
当计时器触发时,会使用与该上下文相关联的取消函数来取消该上下文中运行的所有操作。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
context.WithTimeout
方法用于构造一个 timerCtx
,本质上会调用 context.WithDeadline
方法,截止时间是time.Now().Add(timeout)
.
context.WithDeadlineCause()方法
// not set the cause.
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
//校验 parent context 非空;
if parent == nil {
panic("cannot create context from nil parent")
}
//校验 parent是否可过期, 且过期时间是否早于自己,若是,则构造一个 cancelCtx 返回即可;
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
//构造出一个新的 timerCtx结构体;
c := &timerCtx{
deadline: d,
}
//调用propagateCancel启动守护方法,同步 parent 的 cancel 事件到子 context
c.cancelCtx.propagateCancel(parent, c)
dur := time.Until(d)
//判断过期时间是否已到,若是,直接 cancel timerCtx,并返回 DeadlineExceeded 的错误;
if dur <= 0 {
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
//加锁; c.cancel() 和 time.AfterFunc() 两个操作的原子性和同步性确保在计时器触发前或在取消方法执行后不再触发计时器。
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
//启动time.Timer,达到过期时间后会调用cancel终止该 timerCtx,并返回 DeadlineExceeded 的错误;
//time.AfterFunc(dur, func()) 函数的工作方式是异步的,它创建一个新的 Goroutine 来运行计时器并等待计时器触发。
c.cancel(true, DeadlineExceeded, cause)
})
}
// 返回 timerCtx,以及一个封装了 cancel 逻辑的闭包 cancel 函数.
return c, func() { c.cancel(true, Canceled, nil) }
}
WithDeadline
函数会创建一个新的到期时间戳的上下文。如果在到期时间之前完成操作,则操作正常完成;否则,如果到期时间已经过了,上下文将被标记为超时,并将其传播给与此上下文相关联的所有操作,从而达到错误处理和资源释放的目的。 返回值是一个设置好到期时间戳的子上下文对象,以及一个取消函数(CancelFunc),可以随时用于删除此上下文及其所有子级上下文。创建过程如下:
-
判断parent是否为空,若为空,则引发
panic("cannot create context from nil parent")
-
判断parent是否可过期, 且过期时间是否早于自己,若是,则构造一个
cancelCtx
返回即可; -
构造出一个新的
timerCtx
结构体,并调用propagateCancel
启动守护方法,同步 parent 的 cancel 事件到子context
; -
判断过期时间是否已到,若是,直接
cancel timerCtx
,并返回DeadlineExceeded
的错误; -
加锁;并启动
time.Timer
,这会开启一个新的协程,当Timer
达到过期时间后会调用cancel
终止该timerCtx
,并返回DeadlineExceeded
的err; -
返回
timerCtx
,以及一个封装了cancel
逻辑的闭包cancel
函数.
timerCtx.cancel()方法
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
//复用继承的 cancelCtx 的 cancel 能力,进行 cancel 处理;
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
//判断是否需要手动从 parent 的 children set 中移除,若是则进行处理
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
//停止 time.Timer, 避免timer泄漏
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
由于继承了 cancelCtx
结构体,timerCtx
可以从其父亲结构体获得取消能力,调用cancel()
时过程如下
-
首先调用父亲结构体的
cancel()
函数,不过此时父亲结构体的cancel()
第一个参数removeFromParent
为false
,因为不需要删除父亲结点的所有子结点 -
判断是否需要将该
timerCtx
手动从 parent 的 children set 中移除,若是则进行处理 -
之后加锁,暂停timer计数器并置空,之后解锁返回
valueCtx
valueCtx结构
type valueCtx struct {
Context
key, val any
}
valueCtx
同样继承了一个 parent context;- 一个
valueCtx
中仅有一组 KV值。
WithValue() 方法
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
// 判断Key是不是可以比较的类型
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
// 保存父节点的上下文
return &valueCtx{parent, key, val}
}
valueCtx.Value()方法
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
func value(c Context, key any) any {
//启动一个 for 循环,由下而上,由子及父,依次对 key 进行匹配
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case withoutCancelCtx:
if key == &cancelCtxKey {
// This implements Cause(ctx) == nil
// when ctx is created using WithoutCancel.
return nil
}
c = ctx.c
case *timerCtx:
if key == &cancelCtxKey {
return &ctx.cancelCtx
}
c = ctx.Context
case backgroundCtx, todoCtx:
return nil
default:
return c.Value(key)
}
}
}
-
假如当前
valueCtx
的 key 等于用户传入的 key,则直接返回其 value; -
假如不等,则调用value方法从 parent context 中依次向上寻找,直到找到根节点
emptyctx
,若找不到则返回空。 -
其中
cancelCtx
、timerCtx
、emptyCtx
类型会有特殊的处理方式,如当前key== &cancelCtxKey
,则会返回cancelCtx
自身,而emptyCtx
则会返回空
小结
阅读源码可以看出valueCtx
不适合视为存储介质,存放大量的 kv 数据,原因如下:
-
一个
valueCtx
实例只能存一个kv
对,因此 n 个 kv 对会嵌套 n 个valueCtx
,造成空间浪费; -
基于 k 寻找 v 的过程是线性的,时间复杂度 O(N);
-
不支持基于 k 的去重,相同 k 可能重复存在,并基于起点的不同,返回不同的 v.
由此得知,valueContext
的定位类似于请求头,context
存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
afterFuncCtx(go1.21版本后)
afterFuncCtx结构
type afterFuncCtx struct {
cancelCtx
once sync.Once // either starts running f or stops f from running
f func()
}
afterFuncCtx
同样继承了一个 cancelCtx
;
AfterFunc()方法
// AfterFunc will use it to schedule the call.
func AfterFunc(ctx Context, f func()) (stop func() bool) {
a := &afterFuncCtx{
f: f,
}
a.cancelCtx.propagateCancel(ctx, a)
return func() bool { // 返回stop 调用函数
stopped := false
a.once.Do(func() {
stopped = true
})
if stopped {
a.cancel(true, Canceled, nil)
}
return stopped
}
}
cancel() 方法
func (a *afterFuncCtx) cancel(removeFromParent bool, err, cause error) {
a.cancelCtx.cancel(false, err, cause)
if removeFromParent {
removeChild(a.Context, a)
}
// 开了goroutine去执行函数
a.once.Do(func() {
go a.f()
})
}
总结
总体类图
context使用建议
-
不要在结构体中加入
Context
参数,而是将它显式地传递给需要它的每个函数,并且它应该是第一个参数,通常命名为ctx
。 -
Context
是线程安全的,可以放心地在多个goroutine
中使用。 -
把
Context
传递给多个goroutine
使用时,只要执行一次cancel
操作,所有的goroutine
就可以收到 取消的信号 -
不要把原本可以由函数参数来传递的变量,交给
Context
的Value
来传递。 -
当一个函数需要接收一个
Context
时,但是此时你还不知道要传递什么Context
时,可以先用context.TODO
来代替,而不要选择传递一个nil
。 -
当一个
Context
被cancel
时,继承自该Context
的所有 子Context
都会被cancel
。并且可以多次执行cancel
。