go基础学习之context

世间万事,只该难得,不该易得。易得之事易失去,难得之事难失去

Posted by yishuifengxiao on 2022-08-13

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
2
3
func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}

即使函数允许,也不要传递nil Context。如果您不确定要使用哪个Context,请传递 context.TODO。

使用上下文值仅适用于传输进程和 API 的请求范围数据,而不用于将可选参数传递给函数。

相同的上下文可以传递给在不同 goroutine 中运行的函数; 上下文对于多个 goroutine 同时使用是安全的。

使用原则:

  • 不要把context放到一个结构体中,应该作为第一个参数显式地传入函数
  • 即使方法允许,也不要传入一个nilcontext,如果不确定需要什么context的时候,传入一个context.TODO
  • 使用context的Value相关方法应该传递和请求相关的元数据,不要用它来传递一些可选参数
  • 同样的context可以传递到多个goroutine中,Context在多个goroutine中是安全的
  • 在子context传入goroutine中后,应该在子goroutine中对该子contextDone 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
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
package main

import (
"context"
"fmt"
)

func main() {
// gen在单独的goroutine中生成整数
// 将它们发送到返回的频道。
// gen的调用者需要取消一次该上下文
// 他们完成消耗生成的整数不泄漏
// 内部goroutine由gen开始。
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // 返回不要泄漏goroutine
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
}
}
}

输出如下:

1
2
3
4
5
1
2
3
4
5

2.2 func WithDeadline

1
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

WithDeadline 返回父上下文的副本,并将截止日期调整为不晚于d。如果父母的截止日期早于d,WithDeadline(parent,d)在语义上等同于父母。当截止日期到期,返回的取消功能被调用时,或者父上下文的完成通道关闭时,返回的上下文的完成通道将关闭,以先发生者为准。

取消这个上下文会释放与它相关的资源,所以只要完成在这个Context 中运行的操作,代码就应该调用cancel。

这个例子传递一个任意期限的上下文来告诉阻塞函数,它应该尽快放弃它的工作。

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
package main

import (
"context"
"fmt"
"time"
)

func main() {
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)

// 即使ctx过期,最好还是调用
// 取消功能无论如何。 如果不这样做可能会保留
// 上下文及其父级的活动时间超过必要时间
defer cancel()

select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}

}

输出如下:

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
2
3
4
5
func slowOperationWithTimeout(ctx context.Context) (Result, error) {
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel() // releases resources if slowOperation completes before timeout elapses
return slowOperation(ctx)
}

这个例子传递一个带有超时的上下文来告诉阻塞函数,它应该在超时过后放弃它的工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"context"
"fmt"
"time"
)

func main() {
// 传递带超时的上下文告诉阻塞函数
// 超时过后应该放弃它的工作。
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // 打印"context deadline exceeded"
}

}

输出如下:

1
context deadline exceeded

三 Types

3.1 type CancelFunc

1
type CancelFunc func()

CancelFunc通知操作放弃其工作。CancelFunc不会等待工作停止。在第一次调用之后,对CancelFunc的后续调用不起作用。

3.2 type Context

上下文包含截止日期,取消信号以及跨越 API 边界的其他值。

上下文的方法可能会被多个 goroutine 同时调用。

a

context.Background():可以简单理解我们知道这个上下文要去干什么

context.TODO():可以简单理解我们不清楚要使用哪个上下文、或者还没有可用的上下文

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
type Context interface {
// 截止日期返回代表此情境完成工作的时间
// 应该取消。 如果没有截止日期,截止日期返回 ok == false
// 组,对Deadline的连续调用返回相同的结果。
Deadline() (deadline time.Time, ok bool)

// 完成后,完成返回一个关闭的频道
// 上下文应该取消。 如果可以的话,完成可能返回 nil(零)
// 永远不会被取消。 连续调用完成返回相同的值。
//
// WithCancel 安排在完成取消时关闭完成;
// WithDeadline 安排完成时间截止
// 到期; WithTimeout 安排在完成超时时关闭
// 经过。
//
// Done 提供在select语句中使用:
//
// // Stream 使用 DoSomething 生成值并将它们发送出去
// // 直到 DoSomething 返回一个错误或者 ctx.Done 关闭。
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// 有关如何使用的更多示例,请参阅https://blog.golang.org/pipelines
// 一个Done 通道取消。
Done() <-chan struct{}

// 如果Done 尚未关闭,则Err返回nil。
// 如果完成关闭,则 Err 会返回一个非零错误,以解释原因:
// 如果上下文被取消,则取消
// 或者如果上下文的截止时间已过,则截止时间超过。
// 在Err 返回非零错误之后,对Err 的连续调用返回相同的错误。
Err() error

// 值返回与此上下文关联的值相关的值,或者 nil
// 如果没有值与键关联。 Successive 调用Value
// 相同的密钥返回相同的结果。
//
// 仅将上下文值用于传输的请求范围数据
// 进程和API边界,而不是将可选参数传递给
// 函数。
//
// 一个关键字标识 Context 中的特定值。 希望的Function
// 在Context中存储值通常在全局中分配一个键
// 然后使用该键作为 context.WithValue 和的参数
// Context.Value, 一个密钥可以是支持平等的任何类型;
// 包应该将键定义为未导出的类型以避免
// 碰撞
//
// 定义Context键的软件包应提供类型安全的访问器
// 对于使用该键存储的值:
//

// pac包用户定义了存储在Contexts输入中的用户类型。
//
// import "context"
//
// // 包用户定义了存储在上下文中的用户类型。包用户定义了存储在上下文中的用户类型。
// type User struct {...}
//
// // key(键)是此软件包中定义的键的未导出类型。
// // 这可以防止与其他软件包中定义的键的冲突。
// 键入关键字int
//
// // userKey是用户的关键。上下文中的用户值。 它是
// // 未导出; 客户端使用user.NewContext和user.FromContext
// // 而不是直接使用此密钥。
// var userKey key = 0
//
// // NewContext 返回一个带有值u的新Context。
// func NewContext(ctx context.Context, u *User) context.Context {
// return context.WithValue(ctx, userKey, u)
// }
//
// // FromContext返回存储在ctx中的User值(如果有)。
// func FromContext(ctx context.Context) (*User, bool) {
// u, ok := ctx.Value(userKey).(*User)
// 返回 u, ok
// }
Value(key interface{}) 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
2
3
4
5
6
7
type foo int 
type bar int
m := make(map[interface{}]int)
m[foo(1)] = 1
m[bar(1)] = 2
fmt.Printf("%v", m)
/* map[1:2 1:1] */

可以看到,虽然基础值是相同的,但不同类型的信息会在map中区分它们。由于你为包定义的key类型未导出,因此其他包不会与你在包中生成的key冲突。

由于用于存储数据的key是非导出的,因此我们必须导出执行检索数据的函数。这很容易做到,因为它允许这些数据的使用者使用静态的,类型安全的函数。

这个例子演示了如何将一个值传递给上下文,以及如何在它存在时检索它。

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
package 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"))

}

输出如下:

1
2
found value: Go
key not found: color

总结如下:

  • 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