Context
1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
通过Go源码可知,Context接口需要实现Deadline
,Done
,Err
,Value
四个方法
理解Context的作用之前,你可能更需要知道引入Context解决了什么问题. Context主要是为了解决多个goroutine之间的同步问题. 主要方式就是通过在各个函数间传递Context来共享上下文信息.举个例子:
图 Golang_Without_Context
在没有context的情况下,下层的goroutine实际上感知不了上层的情况,比如最上层是一个http请求,中间有一个数据库redis的请求,下面是一个数据库的请求. 如果最上层的goroutine取消了这次请求,那么下层所有的请求都应该尽快的终止,因为此时返回结果已经没有意义了.
图 Golang_With_Context
OK. 回到Context接口的四个方法里
Deadline
: 返回context结束的时间.Done
: 返回一个Channel,这个Channel会在当前工作完成或者被取消后关闭,多次调用Done方法会返回同一个Channel.Err
:返回context结束的原因Value
:获取对应键的值.同个context多次调用Value
会返回相同的结果.
context标准包中提供了创建Context的一些方法.例如WithTimeout
, WithValue
,WithDeadline
.以WithTimeout
为例来写一个测试用代码.
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
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go handle(ctx, 500*time.Millisecond)
select {
case <-ctx.Done():
fmt.Println("main", ctx.Err())
fmt.Println(ctx.Deadline())
}
}
func handle(ctx context.Context, duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println("handle", ctx.Err())
case <-time.After(duration):
fmt.Println("process request with", duration)
}
}
//go run main.go
process request with 500ms
main context deadline exceeded //错误退出原因为deadline到了.
2021-07-19 16:17:45.374202 +0800 CST m=+1.000266755 true //Deadline返回的结束时间. true表示设置了deadline, 如果context没有设置deadline.则会返回false
上面代码中,通过context.WithTimeout
创建了一个1s内就会被取消的ctx. handle函数会通过select等待两个channel的完成. ctx.Done()会在1s后返回, time.After(duration)会在参数duration的时间后返回.上面的例子中,duration为500ms,因此select会触发第二个case的代码.
如果将handle的duration参数改成1500ms呢?
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
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go handle(ctx, 1500*time.Millisecond)
select {
case <-ctx.Done():
fmt.Println("main", ctx.Err())
fmt.Println(ctx.Deadline())
}
}
func handle(ctx context.Context, duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println("handle", ctx.Err())
case <-time.After(duration):
fmt.Println("process request with", duration)
}
}
//go run main.go
main context deadline exceeded
handle context deadline exceeded
2021-07-19 16:39:18.68905 +0800 CST m=+1.000307493 true
可以看到,因为ctx.Done
先返回,所以打印出了handle context deadline exceeded.
Context的一些使用建议
- Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx.
- Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
- Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
- The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
Context设计的争论点
- 传递context需要在每个函数中显示的作为参数传递,会导致context的泛滥
- 通过context存储键值,如果没有规范,会比较难跟踪.比如多个地方都设了相同的值导致被覆盖这种情况难以排查.