Go Context的使用

总算是理解了 Context

可以算是理解了 Go 的 Context 了吧,大概…
尝试做项目的时候,有时候会碰到 Context 这个东西,但对代码没有什么理解困难,所以就略过没管
但学知识怎么能知其然而不知其所以然呢?所以我今天就跟这个 Context 杠上了,一定要理解它到底是干什么的
学成归来就把现在的理解给记录下来,免得以后又忘了

优雅地结束 gorouting

当一个 gorouting 需要结束时,可以使用 channel + select 来停止它,如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func Select_chan() {
exit := make(chan byte)

go func() { // 创建一个 gorouting
for {
select {
case <-exit: // 当 exit 通道有数据传入时,执行此命令
fmt.Println("exiting...")
return
default:
fmt.Println("goroutine is running...")
time.Sleep(2 * time.Second)
}
}
}()

time.Sleep(10 * time.Second) // 睡眠10秒,模拟程序运行了10秒
fmt.Println("done!")
exit <- 'Y' // 程序执行完成,向exit传入信号以终止程序
}

运行结果很明显,每隔2s就会输出一次 goroutine is running...,后在第10s时向 exit 这个 channel 中传入一个数据,select 中的 case 接受到后就会执行后面的代码,即输出 exiting... 并结束掉这个进程
利用 channel + select 可以很优雅的结束掉进程,这个场景下,也可以使用 Context 来实现

使用 Context 优雅地结束 gorouting

这里使用 Context 很简答,只需要创建一个带结束的 Context,然后把 select 中 case 接受的 channel 改为 ctx.Done(),并在结束进程时调用cancel()就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func Context_fun() {
ctx, cancel := context.WithCancel(context.Background()) // 创建一个带结束的 Context,context.Background()返回一个空Context
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("exiting...")
return
default:
fmt.Println("gorouting running...")
time.Sleep(2 * time.Second)
}
}
}(ctx)
time.Sleep(10 * time.Second)
fmt.Println("done!")
cancel() // 调用取消函数,结束掉 gorouting
time.Sleep(time.Second) // 程序结束过快,还没执行到输出语句就结束了,所以睡眠1秒
}

运行结果和前面的一样

多个 Context 的使用

Context 是一个树形结构的调用关系,每一个 Context 都有自己的父 Context,而根 Context 可以通过 context.Background() 来创建
类似于进程,当父进程结束时,它的子进程也要结束并回收资源
Context 也是如此,之所以需要使用 Context 是因为 gorouting 创建之后可能不受控制的无限进行下去,导致程序崩溃
使用 Context 可以设置在某一条件满足的情况下,结束掉它的父 Context,不管有多少个子 Context 都可以一次性给他结束掉
如下代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func Multi_Context() {
ctx, cancel := context.WithCancel(context.Background())
go Just_run(ctx, "one")
go Just_run(ctx, "two")
go Just_run(ctx, "three")
time.Sleep(10 * time.Second)
cancel()
time.Sleep(time.Second)
}

func Just_run(ctx context.Context, name string) { // 当传入的参数中有 Context 时,把他放在第一位
for {
select {
case <-ctx.Done():
fmt.Println(name + " has done!")
return
default:
fmt.Println(name + " is running!")
time.Sleep(2 * time.Second)
}
}
}

创建一个带 cancel 的 Context,把它传给了三个进程,当父 Context 结束完成时,也就是10s后,执行一次 cancel() 结束掉这个 Context ,三个 gorouting 也就一起被结束掉了
程序的运行结果如下

1
2
3
4
5
6
7
one is running!
two is running!
three is running!
...
two has done!
one has done!
three has done!

因为 gorouting 类似于操作系统中的进程调用,当有多个进程处于就绪态时,随机选取一个进程进入运行态,所以 one,two,three 的先后输出都是随机的

Context 接口

Context 接口中有四个方法

1
2
3
4
5
6
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}

Deadline() 需要配合设置了时间的 Context 来使用,返回这个 Context 应该被取消的时间,当 ok 为 false 时表示没有设置结束时间
Done() 最常用的方法,结束掉此 Context
Err() 返回 Context 被取消的原因,如果 Context 还没有 Done() 则返回 nil,如果被 Done() 了则返回原因,比如时间到了
Value() 需要配合可以传值的 Context 来使用,在使用 Context 时可以顺便带个值进来

context.Background(),它是用来作为根 Context 来使用的,相当于操作系统中的0号进程
context.TODO(),当不知道使用哪个 Context 时可以使用它,我觉得可以作为叶子节点的 Context 来使用

Context 的 With 方法

如过需要创建子 Context ,那么就需要用到这4个 With 方法

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

WithCancel

这个方法前面已经多次使用了,就是创建一个带 cancel 的 Context,可以通过 cancel() 来结束这个 Context

WithDeadline

这个方法用来创建一个带结束时间的 Context,注意是结束时间,它用的参数是 time.Time,意思是在几点几分几秒结束掉这个 Context
示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func Context_WithDeadline() {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(20*time.Second)) // 创建一个可以随时取消的,20秒后自动结束的 Context
defer cancel() // 最后取消所有的 Context
go Just_run(ctx, "one")
go Just_run(ctx, "two")
go Just_run(ctx, "three")
time.Sleep(4 * time.Second)
fmt.Println("done! do not run anymore!")
cancel()
}

func Just_run(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name + " has done!")
return
default:
fmt.Println(name + " is running!")
time.Sleep(2 * time.Second)
}
}
}

在上面的代码中创建了一个以现在时间为基准,20s后自动结束的 Context,然后把他传给三个 gorouting,但假设程序执行4s后就结束了,和 WithCancel 方法一样,调用 cancel 就可以直接结束掉这个 Context
如果把时间改为 > 20s 那么 Context 会在20s时,无论程序是否执行完毕,都会自动取消

WithTimeout

这个方法和 WithDeadline 没什么区别,就是传入的时间不是一个时间点,而是一个 time.Duration 时间段,比如10s,20s
示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func Context_WithTimeout() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
go Just_run(ctx, "one")
go Just_run(ctx, "two")
go Just_run(ctx, "three")
time.Sleep(15 * time.Second)
fmt.Println("done!")
}

func Just_run(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name + " has done!")
return
default:
fmt.Println(name + " is running!")
time.Sleep(2 * time.Second)
}
}
}

上面的代码创建了一个持续时间为10s的 Context,然后把他传给三个 gorouting,假设程序需要15s 才鞥结束,但是10s后,Context 就会自动结束,如果在10s之前,就可以通过 cancel() 来结束掉这个 Context

WithValue

这个方法和前面三个不太一样,它可以创建一个带键值对的 Context,在使用这个 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
var key = "name" // 定义一个键

func Context_WithValue() {
ctx, cancel := context.WithCancel(context.Background())
ctx2 := context.WithValue(ctx, key, "one") // 创建一个带值的 Context
ctx3 := context.WithValue(ctx, key, "two")
ctx4 := context.WithValue(ctx, key, "three")
go Always_run(ctx2)
go Always_run(ctx3)
go Always_run(ctx4)
time.Sleep(10 * time.Second)
fmt.Println("all done!")
cancel()
time.Sleep(time.Second)
}

func Always_run(ctx context.Context) { // 这里的参数只用了 Context,前面能输出名字是因为传入了 name 参数
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Value(key), " has done!") // 使用 ctx.Value() 来获取值
return
default:
fmt.Println((ctx.Value(key)), " is running!")
time.Sleep(2 * time.Second)
}
}
}

如上代码,创建一个带取消的 Context,然后用 WithValue 来创建三个带键值的 Context
分别把三个 Context 传入三个 gorouting,这样每一个 gorouting 都可以输出不同的结果了
运行结果

1
2
3
4
5
6
7
8
three  is running!
one is running!
two is running!
...
all done!
three has done!
two has done!
one has done!

总算是把 Context 这个给解决了 o(^▽^)o
之前就总是想为什么需要用到 Context 呢,经过了这段时间的学习,发现确实需要用到 Context
使用 Context 可以更优雅的创建和停止 gorouting,不至于创建了 gorouting 就不管了,这样系统占用就不会越来越多,最终导致崩溃
所以 Go 语言中使用这一机制是十分简单有效的,理解起来也不难 (๑¯∀¯๑)