文章目录
What is context
Context 中文为 上下文
,可以用来简化单个请求多个 goroutine 之间关于数据共享,取消信号,截止时间等操作。
Context 结构
Context 在 golang 工具包中,是一个 interface,其结构如下:
type Context interface {
// 获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
Deadline() (deadline time.Time, ok bool)
// 返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。
Done() <-chan struct{}
// 返回取消的错误原因,因为什么Context被取消
Err() error
// 获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
Value(key interface{}) interface{}
}
Context 接口并不需要我们实现,golang 内置了 2 个实现,可以把这 2 个内置的 context 作为顶层的 parent context,基于 parent context 继承派生 children context
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
一个是
Background
,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。一个是
TODO
,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。他们两个本质上都是
emptyCtx
结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
Context 的继承派生
context 包提供了以下 with
函数来派生 context
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
Why use context
select + channel 协程间的通信
func communicateWithChannel(stop <-chan bool) {
for {
select {
case <-stop:
fmt.Println("children thread exiting...")
return
default:
fmt.Println("children thread is doing task...")
time.Sleep(2 * time.Second)
}
}
}
func TestChannel(t *testing.T) {
stop := make(chan bool)
go communicateWithChannel(stop)
time.Sleep(time.Duration(10) * time.Second)
stop <- true
time.Sleep(time.Duration(10) * time.Second)
fmt.Println("main thread is done")
}
通过 channel 虽然可以实现,父协程与子协程之间的通信,但是当 goroutine 中又开启新的 goroutine,形成树状结构时,便很难通过 channel 来控制子协程的结束。
How to use
func contextWithCancel(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("children thread exiting...")
return
default:
fmt.Println("children thread is doing task...")
time.Sleep(2 * time.Second)
}
}
}
func contextWithValue(ctx context.Context) {
for {
val := ctx.Value(ctxKeyName)
select {
case <-ctx.Done():
fmt.Printf("children %d thread exiting...\n", val)
return
default:
fmt.Printf("children %d thread is doing task...\n", val)
time.Sleep(2 * time.Second)
}
}
}
func TestContextWithCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
go contextWithCancel(ctx)
time.Sleep(time.Duration(10) * time.Second)
cancel()
time.Sleep(time.Duration(10) * time.Second)
fmt.Println("main thread is done")
}
func TestContextWithValue(t *testing.T) {
ctx, _ := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
for i := 1; i <= 3; i++ {
subCtx := context.WithValue(ctx, ctxKeyName, i)
go contextWithValue(subCtx)
}
time.Sleep(time.Duration(15) * time.Second)
fmt.Println("main thread is done")
}
Context 使用原则
- 不要把Context放在结构体中,要以参数的方式传递
- 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
- Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
- Context是线程安全的,可以放心的在多个goroutine中传递
Ref: https://www.flysnow.org/2017/05/12/go-in-action-go-context.html