英文原版context package - context - Go Packages
简介
context 上下文
上下文定义了上下文类型,它跨API边界和进程之间传递截止日期、取消信号和其他请求范围的值。对服务器的传入请求应创建上下文,对服务器的传出调用应接受上下文。。当上下文被取消时,从它派生的所有上下文也被取消(层层传递,级联取消)。未调用CancelFunc会泄漏子级及其子级,直到父级被取消或计时器触发。
使用上下文的程序应遵循以下规则,
- 以保持跨包的接口一致,并启用静态分析工具来检查上下文传播
- 不要在结构类型内存储上下文;相反,将上下文显式传递给每个需要它的函数。
它应该在程序中流动,从一个函数传递到另一个函数,并根据需要进行扩展。理想情况下,每个请求开的时候都会创建一个 context 对象,层层传递,并在请求结束时结束。
- 所有阻塞操作 及耗时较长操作都应该支持取消功能
- 函数调用链使用上下文进行传播,在传播过程中可以选择将其替换为使用WithCancel、WithDeadline、WithTimeout或WithValue创建的派生上下文
- 上下文应该是第一个参数,通常命名为ctx:
func DoSomething(ctx context.Context, arg Arg) error {
// ... 使用上下文 ...
}
即使函数允许,也不要传递nil上下文。如果您不确定要使用哪个上下文,请传递context.TODO。
上下文值仅用于传输进程和API的请求范围数据,不要用于将可选参数传递给函数。
相同的上下文可以传递给在不同goroutine中运行的函数;上下文对于多个goroutine并发是安全的。
funcWithCancel
级联取消
当一个 context 被取消时,从它派生的所有 context 也将被取消。WithCancel(ctx) 参数 ctx 认为是 parent ctx,在内部会进行一个传播关系链的关联。Done() 返回 一个 chan,当我们取消某个parent context, 实际上上会递归层层 cancel 掉自己的 child context 的 done chan 从而让整个调用链中所有监听 cancel 的 goroutine 退出。。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
取消此上下文将释放与之相关的资源,因此代码应该在该上下文中运行的操作完成后立即调用cancel。
如果要实现一个超时控制,通过上面的 context 的 parent/child 机制,其实我们只需要启动一个定时器,然后在超时的时候,直接将当前的 context 给 cancel 掉,就可以实现监听在当前和下层的额context.Done() 的 goroutine 的退出
例子一
此示例演示了如何使用context.WithCancel()来防止goroutine泄漏。在示例函数结束时,gen启动的goroutine将返回而不会泄漏。
package main
import (
"context"
"fmt"
)
func main() {
//gen在单独的goroutine中生成整数,并将它们发送到返回的通道。gen的调用者需要取消一次上下文它们消耗生成的整数以避免泄漏
//gen。
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // 返回不会导致资源泄露
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 当我们停止遍历整数后进行取消
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
}
funcWithDeadline
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithDeadline返回父上下文的副本,截止时间调整为不晚于d。如果父上下文的最后期限早于d,则WithDeadline(parent,d)在语义上等同于parent。当截止日期过期、调用返回的cancel函数或父上下文的Done通道关闭时(以先发生的为准),返回的上下文的Done通道关闭。取消此上下文将释放与之相关的资源,因此代码应该在该上下文中运行的操作完成后立即调用cancel。
例子
pack(
"context"
"fmt"
"time"
)
const shortDuration = 1 * time.Millisecond
func main() {
d := time.Now().Add(shortDuration)
ctx, cancel := context.WithDeadline(context.Background(), d)
//尽管ctx将过期,但最好的做法是将取消放在所有情况里面比如说defer。否则可能会导致上下文及其父对象的生存时间超过了必要的时间。
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
程序会输出 context deadline exceeded
func WithTimeout
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout返回WithDeadline(parent,time.Now().Add(timeout))
取消此上下文将释放与其相关的资源,因此代码应在该上下文中运行的操作完成后立即调用cancel:
如果慢操作在超时之前完成,则释放资源
func slowOperationWithTimeout(ctx context.Context) (Result, error) {
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel() //
return slowOperation(ctx)
}
例子
func main() {
//传递一个带有超时控制的上下文,告诉阻塞函数它应在超时后放弃其工作。
ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // 打印 "context deadline exceeded"
}
}
func Background() Context
background函数返回非零的空上下文。它不能被直接取消,没有值,也没有过期时间。它通常由主函数、初始化和测试使用,并作为传入请求的根上下文。
func TODO()Context
TODO返回一个非零的空上下文。代码应该使用上下文。当不清楚要使用哪个上下文或它还不可用时(因为周围的函数还没有扩展到接受上下文参数)。
func WithValue(parent Context, key, val any) Context
WithValue返回父级上下文的副本,其中与键关联的值为val。上下文值仅用于传输进程和API的请求范围数据(比如说链路追踪id),而不用于将可选参数传递给函数。提供的键必须具有可比性,值数据必须是并发安全的,因为某些场景会并发读写,以避免使用上下文的包之间发生数据竞争。
注意点
context value 应该是 不变 的,每次重新赋值应该是新的 context,即: context.WithValue(ctx, oldvalue)
例子
ackage main
import (
"context"
"fmt"
)
func main() {
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
}
本例演示了如何将值传递给上下文,以及如何在存在值的情况下检索该值。
如我们新建了一个基于 context.Background() 的 ctx1,携带了一个 map 的数据,map 中包含了 “k1”: “v1” 的一个键值对,ctx1 被两个 goroutine 同时使用作为函数签名传入,如果我们修改了 这个map,会导致另外进行读 context.Value 的 goroutine 和修改 map 的 goroutine,在 map 对象上产生 data race。因此我们要使用 copy-on-write 的思路,解决跨多个 goroutine 使用数据、修改数据的场景。
copy-on-write : 从 ctx1 中获取 map1(可以理解为 v1 版本的 map 数据)。构建一个新的 map 对象 map2,复制所有 map1 数据,同时追加新的数据 “k2”: “v2” 键值对,使用 context.WithValue 创建新的 ctx2,ctx2 会传递到其他的 goroutine 中。这样各自读取的副本都是自己的数据,写行为追加的数据,在 ctx2 中也能完整读取到,同时也不会污染 ctx1 中的数据。