Context 包定义了上下文类型,该上下文类型跨越 API 边界和进程之间传递截止期限,取消信号和其他请求范围值。
对服务器的传入请求应创建一个 Context,对服务器的传出调用应接受 Context。它们之间的函数调用链必须传播 Context,可以用使用 WithCancel,WithDeadline,WithTimeout 或WithValue创建的派生上下文替换。当 Context 被取消时,从它派生的所有 Context 也被取消。
WithCancel,WithDeadline 和 WithTimeout 函数采用Context(父级)并返回派生的Context(子级)和CancelFunc。调用 CancelFunc 将取消子对象及其子对象,删除父对子对象的引用,并停止任何关联的定时器。未能调用CancelFunc 会泄漏子项及其子项,直至父项被取消或计时器激发。go vet 工具检查在所有控制流路径上使用 CancelFuncs。
使用 Contexts 的程序应该遵循这些规则来保持包之间的接口一致,并使静态分析工具能够检查上下文传播:
不要将上下文存储在结构类型中;相反,将一个 Context 明确地传递给每个需要它的函数。上下文应该是第一个参数,通常命名为 ctx:
1 | func DoSomething(ctx context.Context, arg Arg) error { |
即使函数允许,也不要传递nil Context。如果您不确定要使用哪个Context,请传递 context.TODO。
使用上下文值仅适用于传输进程和 API 的请求范围数据,而不用于将可选参数传递给函数。
相同的上下文可以传递给在不同 goroutine 中运行的函数; 上下文对于多个 goroutine 同时使用是安全的。
使用原则:
- 不要把
contex
t放到一个结构体中,应该作为第一个参数显式地传入函数 - 即使方法允许,也不要传入一个
nil
的context
,如果不确定需要什么context
的时候,传入一个context.TODO
- 使用
context
的Value相关方法应该传递和请求相关的元数据,不要用它来传递一些可选参数 - 同样的
context
可以传递到多个goroutine
中,Context
在多个goroutine
中是安全的 - 在子
context
传入goroutine
中后,应该在子goroutine
中对该子context
的Done channel
进行监控,一旦该channel
被关闭,应立即终止对当前请求的处理,并释放资源。
一 变量
Canceled 是上下文取消时 Context.Err 返回的错误。
1 | var Canceled = errors.New("context canceled") |
DeadlineExceeded 是 Context.Err 在上下文截止时间过后返回的错误。1
var DeadlineExceeded error = deadlineExceededError{}
二 函数
2.1 func WithCancel
1 | func WithCancel(parent Context) (ctx Context, cancel CancelFunc) |
WithCancel 返回一个新的完成通道的父级的副本。返回的上下文的 Done 通道在返回的取消函数被调用时或父上下文的 Done 通道关闭时关闭,无论哪个先发生。
取消这个上下文会释放与它相关的资源,所以只要完成在这个Context 中运行的操作,代码就应该调用 cancel。
这个例子演示了使用可取消上下文来防止 goroutine 泄漏。在示例函数结束时,由 gen 启动的 goroutine 将返回而不会泄漏。
1 | package main |
输出如下:
1 | 1 |
2.2 func WithDeadline
1 | func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) |
WithDeadline 返回父上下文的副本,并将截止日期调整为不晚于d。如果父母的截止日期早于d,WithDeadline(parent,d)在语义上等同于父母。当截止日期到期,返回的取消功能被调用时,或者父上下文的完成通道关闭时,返回的上下文的完成通道将关闭,以先发生者为准。
取消这个上下文会释放与它相关的资源,所以只要完成在这个Context 中运行的操作,代码就应该调用cancel。
这个例子传递一个任意期限的上下文来告诉阻塞函数,它应该尽快放弃它的工作。
1 | package main |
输出如下:
1 | context deadline exceeded |
2.3 func WithTimeout
1 | func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) |
WithTimeout
返回WithDeadline(parent, time.Now().Add(timeout))
。
取消这个上下文可以释放与它相关的资源,因此只要在这个Context 中运行的操作完成,代码就应该立即调用 cancel:
1 | func slowOperationWithTimeout(ctx context.Context) (Result, error) { |
这个例子传递一个带有超时的上下文来告诉阻塞函数,它应该在超时过后放弃它的工作。
1 | package main |
输出如下:
1 | context deadline exceeded |
三 Types
3.1 type CancelFunc
1 | type CancelFunc func() |
CancelFunc通知操作放弃其工作。CancelFunc不会等待工作停止。在第一次调用之后,对CancelFunc的后续调用不起作用。
3.2 type Context
上下文包含截止日期,取消信号以及跨越 API 边界的其他值。
上下文的方法可能会被多个 goroutine 同时调用。
context.Background():可以简单理解我们知道这个上下文要去干什么
context.TODO():可以简单理解我们不清楚要使用哪个上下文、或者还没有可用的上下文
1 | type Context interface { |
Context
接口包含四个方法:
Deadline
返回绑定当前context
的任务被取消的截止时间;如果没有设定期限,将返回ok == false
。Done
当绑定当前context
的任务被取消时,将返回一个关闭的channel
;如果当前context
不会被取消,将返回nil
。Err
如果Done
返回的channel
没有关闭,将返回nil
;如果Done
返回的channel
已经关闭,将返回非空的值表示任务结束的原因。如果是context
被取消,Err
将返回Canceled
;如果是context
超时,Err
将返回DeadlineExceeded
。Value
返回context
存储的键值对中当前key
对应的值,如果没有对应的key
,则返回nil
。
3.2.1 func Background
1 | func Background() Context |
Background返回non-nil(非零),空的 Context。它从未被取消,没有值,也没有最后期限。它通常由主函数,初始化和测试使用,并作为传入请求的top-level Context (顶级上下文)
3.2.2 func TODO
1 | func TODO() Context |
TODO 返回非零空的上下文。代码应该使用context.TODO,当它不清楚使用哪个 Context或它尚不可用时(因为周围的函数尚未扩展为接受Context参数)。TODO 被静态分析工具识别,以确定上下文是否在程序中正确传播。
3.2.3 func WithValue
1 | func WithValue(parent Context, key, val any) Context |
WithValue 返回父键的副本,其中与键关联的值是val。
使用上下文值仅适用于传输进程和 API 的请求范围数据,而不用于将可选参数传递给函数。
提供的密钥必须具有可比性,不应该是字符串类型或任何其他内置类型,以避免使用上下文的包之间发生冲突。WithValue 的用户应该为键定义他们自己的类型。为了避免在分配给接口时分配{},上下文键通常具有具体类型 struct {}。或者,导出的上下文关键字变量的静态类型应该是指针或接口。
使用的key必须是可比较的,也就是说== 和 != 必须能返回正确的结果
返回值必须是并发安全的,这样才能从多个goroutine访问
- 由于Context的Value(key interface{}) interface{} 键和值都被定义为interface{},当试图检索值时,会失去其类型安全性。基于此,Go建议在context中存储和检索值时遵循一些规则。
- 推荐在包中自行定义key的类型,这样无论是否其他包执行相同的操作都可以防止context中的冲突。看下面这个例子:
1 | type foo int |
可以看到,虽然基础值是相同的,但不同类型的信息会在map中区分它们。由于你为包定义的key类型未导出,因此其他包不会与你在包中生成的key冲突。
由于用于存储数据的key是非导出的,因此我们必须导出执行检索数据的函数。这很容易做到,因为它允许这些数据的使用者使用静态的,类型安全的函数。
这个例子演示了如何将一个值传递给上下文,以及如何在它存在时检索它。
1 | package main |
输出如下:
1 | found value: Go |
总结如下:
context
包通过构建树形关系的context
,来达到上一层goroutine
对下一层goroutine
的控制。对于处理一个request
请求操作,需要通过goroutine
来层层控制goroutine
,以及传递一些变量来共享。context
变量的请求周期一般为一个请求的处理周期。即针对一个请求创建context
对象;在请求处理结束后,撤销此ctx变量,释放资源。- 每创建一个
goroutine
,要不将原有context
传递给子goroutine
,要么创建一个子context
传递给goroutine
. Context
能灵活地存储不同类型、不同数目的值,并且使多个Goroutine
安全地读写其中的值。- 当通过父
Context
对象创建子Context
时,可以同时获得子Context
的撤销函数,这样父goroutine
就获得了子goroutine
的撤销权。
参考资料
https://cloud.tencent.com/developer/section/1140611