0%

golang的context包使用场景

1. 介绍

一个网络请求Request,每个Request都需要开启一个goroutine做一些事情,这些goroutine又可能会开启其他的goroutine。所以我们需要一种可以跟踪goroutine的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的Context,称之为上下文非常贴切,它就是goroutine的上下文。

1.1 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{}
}
  • Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。

  • Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。

  • Err方法返回取消的错误原因,因为什么Context被取消。

  • Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。

1.2 Context的继承衍生

1
2
3
4
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

2. 使用

2.1 value传递数据

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

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

var key string = "name"

func main() {
ctx, cancel := context.WithCancel(context.Background())
valueCtx := context.WithValue(ctx, key, "监控1")
go watch(valueCtx)

time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")

cancel()
time.Sleep(5 * time.Second)
}
func watch(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Value(key), "监控退出,停止了...")
return
default:
fmt.Println(ctx.Value(key), "goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}


监控1 goroutine监控中...
监控1 goroutine监控中...
监控1 goroutine监控中...
监控1 goroutine监控中...
监控1 goroutine监控中...
可以了,通知监控停止
监控1 监控退出,停止了...

2.2 控制gorotuine退出

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

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

func main() {
ctx, cancel := context.WithCancel(context.Background())
go watch(ctx, "监控1")
go watch(ctx, "监控2")
go watch(ctx, "监控3")
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
cancel()

time.Sleep(5 * time.Second)
}
func watch(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "监控退出,停止了...")
return
default:
fmt.Println(name, "goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}


监控3 goroutine监控中...
监控2 goroutine监控中...
监控1 goroutine监控中...
监控3 goroutine监控中...
监控2 goroutine监控中...
监控1 goroutine监控中...
监控3 goroutine监控中...
监控2 goroutine监控中...
监控1 goroutine监控中...
监控1 goroutine监控中...
监控2 goroutine监控中...
监控3 goroutine监控中...
监控1 goroutine监控中...
监控2 goroutine监控中...
监控3 goroutine监控中...
可以了,通知监控停止
监控3 监控退出,停止了...
监控1 监控退出,停止了...
监控2 监控退出,停止了...

示例中启动了3个监控goroutine进行不断的监控,每一个都使用了Context进行跟踪,当我们使用cancel函数通知取消时,这3个goroutine都会被结束。这就是Context的控制能力,它就像一个控制器一样,按下开关后,所有基于这个Context或者衍生的子Context都会收到通知,这时就可以进行清理操作了,最终释放goroutine,这就优雅的解决了goroutine启动后不可控的问题。

2.3 总结

  • 不要把Context放在结构体中,要以参数的方式传递
  • 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  • Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  • Context是线程安全的,可以放心的在多个goroutine中传递
给作者打赏,可以加首页微信,咨询作者相关问题!