context 学习
比如,服务端接收到客户端的 HTTP 请求之后,可以把客户端的 IP 地址和端口、客户端的
身份信息、请求接收的时间、Trace ID 等信息放入到上下文中,这个上下文可以在后端的
方法调用中传递,后端的业务方法除了利用正常的参数做一些业务处理(如订单处理)之
外,还可以从上下文读取到消息请求的时间、Trace ID 等信息,把服务处理的时间推送到
Trace 服务中。Trace 服务可以把同一 Trace ID 的不同方法的调用顺序和调用时间展示成
流程图,方便跟踪。
不过,Go 标准库中的 Context 功能还不止于此,它还提供了超时(Timeout)和取消
(Cancel)的机制,下面就让我一一道来。
Go 的开发者也注意到了“关于 Context,存在一些争议”这件事儿,所以,Go 核心开发
者 Ian Lance Taylor 专门开了一个 issue 28342
,用来记录当前的 Context 的问题:
Context 包名导致使用的时候重复 ctx context.Context;
Context.WithValue 可以接受任何类型的值,非类型安全;
Context 包名容易误导人,实际上,Context 最主要的功能是取消 goroutine 的执行;
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{}
}
|
context的使用
首先使用context
实现文章开头done channel
的例子来示范一下如何更优雅实现协程间取消信号的同步:
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
|
func main() {
messages := make(chan int, 10)
// producer
for i := 0; i < 10; i++ {
messages <- i
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
// consumer
go func(ctx context.Context) {
ticker := time.NewTicker(1 * time.Second)
for _ = range ticker.C {
select {
case <-ctx.Done():
fmt.Println("child process interrupt...")
return
default:
fmt.Printf("send message: %d\n", <-messages)
}
}
}(ctx)
defer close(messages)
defer cancel()
select {
case <-ctx.Done():
time.Sleep(1 * time.Second)
fmt.Println("main process exit!")
}
}
|
这个例子中,只要让子线程监听主线程传入的ctx
,一旦ctx.Done()
返回空channel
,子线程即可取消执行任务。但这个例子还无法展现context
的传递取消信息的强大优势。
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
|
package main
import (
"context"
"log"
"os"
"time"
)
var logg *log.Logger
func someHandler() {
ctx, cancel := context.WithCancel(context.Background())
go doStuff(ctx)
//10秒后取消doStuff
time.Sleep(10 * time.Second)
// 取消 ctx ,用于超时控制
cancel()
}
//每1秒work一下,同时会判断ctx是否被取消了,如果是就退出
func doStuff(ctx context.Context) {
for {
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
logg.Printf("done")
return
default:
logg.Printf("work")
}
}
}
func main() {
logg = log.New(os.Stdout, "", log.Ltime)
someHandler()
logg.Printf("down")
}
|
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"
"time"
)
func timeoutHandler() {
// 创建继承Background的子节点Context
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
go doSth(ctx)
//模拟程序运行 - Sleep 10秒
time.Sleep(10 * time.Second)
cancel() // 3秒后将提前取消 doSth goroutine
}
//每1秒work一下,同时会判断ctx是否被取消,如果是就退出
func doSth(ctx context.Context) {
var i = 1
for {
time.Sleep(1 * time.Second)
select {
case <-ctx.Done():
fmt.Println("done")
return
default:
fmt.Printf("work %d seconds: \n", i)
}
i++
}
}
func main() {
fmt.Println("start...")
timeoutHandler()
fmt.Println("end.")
}
|
Context
机制最核心的功能是在goroutine之间传递cancel信号,但是它的实现是不完全的。
Cancel可以细分为主动与被动两种,通过传递context参数,让调用goroutine可以主动cancel被调用goroutine。但是如何得知被调用goroutine什么时候执行完毕,这部分Context机制是没有实现的。而现实中的确又有一些这样的场景,比如一个组装数据的goroutine必须等待其他goroutine完成才可开始执行,这是context明显不够用了,必须借助 sync.WaitGroup
。