1. 什么是 Context?
Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。
context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。
Context,也叫上下文,它的接口定义如下
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
可以看到 Context 接口共有 4 个方法
-
Deadline:返回的第一个值是
截止时间
,到了这个时间点,Context 会自动触发
Cancel 动作。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context。 -
Done:返回一个
只读的通道
(只有在被cancel后才会返回),类型为 struct{}。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。 -
Err:返回 context 被 cancel 的原因。
-
Value:返回被绑定到 Context 的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
2. 为什么要用Context
- 用于控制goroutine的结束,但它解决的并不是
能不能
的问题,而是解决更好用
的问题。
2.1. 当不用Context时,利用channel+select来主动让goroutine停止
示例1
package main
import (
"fmt"
"time"
)
/*
1. 利用channel控制goroutine的停止
*/
func main() {
stopChan := make(chan bool)
go func() {
for {
select {
case <-stopChan:
fmt.Println("goroutin1 exit.")
return
default:
fmt.Println("goroutin1 sleep 1s, keep going.")
time.Sleep(time.Second * 2)
}
}
}()
go func() {
for {
select {
case <-stopChan:
fmt.Println("goroutin2 exit.")
return
default:
fmt.Println("goroutin2 sleep 1s, keep going.")
time.Sleep(time.Second * 3)
}
}
}()
time.Sleep(10 * time.Second)
fmt.Println("10s 时间到了,主进程需要退出了.")
// 发送信号让goroute1结束
stopChan <- true
// 发送信号让goroute2结束
stopChan <- true
time.Sleep(5 * time.Second)
}
示例2
package main
import (
"fmt"
"time"
)
/*
1. 利用关闭channel的方法,让2个goroutine同时结束
*/
func main() {
stopChan := make(chan bool)
go func() {
for {
select {
case <-stopChan:
fmt.Println("goroutin1 exit.")
return
default:
fmt.Println("goroutin1 sleep 1s, keep going.")
time.Sleep(time.Second * 2)
}
}
}()
go func() {
for {
select {
case <-stopChan:
fmt.Println("goroutin2 exit.")
return
default:
fmt.Println("goroutin2 sleep 1s, keep going.")
time.Sleep(time.Second * 3)
}
}
}()
time.Sleep(10 * time.Second)
fmt.Println("10s 时间到了,主进程需要退出了.")
// 利用关闭channel的方法,让2个goroutine同时结束
close(stopChan)
time.Sleep(5 * time.Second)
}
2.2 使用context来主动让goroutine停止
先ctx, cancel := context.WithCancel(context.Background()) 创建一个ctx实例
再利用cancel()函数执行控制goroutine的停止
package main
import (
"context"
"fmt"
"time"
)
/*
// 利用context,手动让2个goroutine同时结束[是不是更简单?]
*/
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("goroutin1 exit.")
return
default:
fmt.Println("goroutin1 sleep 1s, keep going.")
time.Sleep(time.Second * 1)
}
}
}()
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("goroutin2 exit.")
return
default:
fmt.Println("goroutin2 sleep 1s, keep going.")
time.Sleep(time.Second * 1)
}
}
}()
time.Sleep(10 * time.Second)
fmt.Println("10s 时间到了,goroutine需要退出了.")
// 利用context的方法,手动让2个goroutine同时结束
cancel()
time.Sleep(5 * time.Second)
}
2.3 使用context实现goroutine的超时控制
- 使用场景:让goroutine执行一个任务,如果在指定时间内没有完成,这利用context的WithTimeout()主动让goroutine退出
package main
import (
"fmt"
"time"
"context"
)
// 场景: 如果你需要对一个用协程启动的函数做超时控制,可以用context来完成goroutine的控制
func main() {
// 设置一个用于超时控制的context ctx, ctx作为参数可以用来作为协程的超时控制
ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)
defer cancel()
// ctx作为参数传递给需要做超时控制的函数
go Monitor(ctx)
time.Sleep(20 * time.Second)
}
func Monitor(ctx context.Context) {
for {
select {
// 如果context 超时,ctx.Done()就会返回一个空接口 struct{}
case <- ctx.Done():
// 如果超时时间到了,就退出循环
fmt.Println(ctx.Err())
return
// 如果没有超时,打印输出后继续循环
default:
time.Sleep(1*time.Second)
fmt.Println("monitor")
}
}
}
2.4 利用context向goroutine传递参数
- 除了超时控制与主动停止goroutine,还有可以通过Context传递上下文变量给其他协程。这样可以避免在协程之间传递大量的变量,代码更整洁可维护。下面的例子通过WithValue传递给协程一个变量,并且通过channel在协程之间通信。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 为ctx设置一个key-value
ctx := context.Background()
ctx = context.WithValue(ctx, "hello", "world")
x := ctx.Value("hello")
fmt.Println("x=", x) // world
// 将key-vluae值传递到goroutine
go work(ctx)
time.Sleep(3 * time.Second)
}
func work(ctx context.Context) {
fmt.Println("do worker.")
fmt.Println("hello=", ctx.Value("hello")) // world,利用context传递key-value
// 继续传递到下层goroutine
go subwork(ctx)
}
func subwork(ctx context.Context) {
fmt.Println("do subwork.")
fmt.Println("hello=", ctx.Value("hello")) // world,利用context传递key-value到更进一层
}
程序输出:
x= world
do worker.
world
do subwork.
world