什么是context?
context(上下文)可以理解为是承接上下文的载体,可以被无限传递下去,Go中的context负责存放协程的当前信息(快照),其中包含着协程中的变量信息及函数调用。
context的使用场景
后端接收请求时,有时要将获取到的数据交由多个协程处理。例如登录验证时,将权限验证、密码验证、有效期验证分到三个不同的协程里处理,如果此时有一个协程处理失败了,其他协程也应该立即关闭,避免持续占用系统资源。而在Go中就可以用context来进行控制操作。
context的底层结构
// Context提供跨越API的截止时间获取,取消信号,以及请求范围值的功能。
// 它的这些方案在多个goroutine中使用是安全的
type Context interface {
// 如果设置了截止时间,这个方法ok会是true,并返回设置的截止时间
Deadline() (deadline time.Time, ok bool)
// 如果Context超时或者主动取消就返回一个关闭的channel,如果返回的是nil,表示这个context永远不会关闭,例如:Background()
Done() <-chan struct{}
// 返回发生的错误
Err() error
// 它的作用就是传值,保存context的相关数据
Value(key interface{}) interface{}
}
Done方法在Context被取消或超时时返回一个close的channel,close的channel可以作为广播通知,告诉给context相关的函数要停止当前工作然后返回。
context的调用
context的调用是链式/树状的,如果一个节点被取消,该节点旗下的所有子context都将会被取消。
如何创建一个context树
创建一个context根节点(Background)
// Background returns an empty Context. It is never canceled, has no deadline,
// and has no values. Background is typically used in main, init, and tests,
// and as the top-level Context for incoming requests.
func Background() Context
Backgound返回的Context是所有context的root,不能够被cancel。
该Context通常由接收request的第一个goroutine创建,它不能被取消、没有值、也没有过期时间,常作为处理request的顶层context存在。
创建一个继承父节点的context(WithCancel、WithDeadline、WithTimeout、WithValue)
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
WithXXX返回一个继承父节点的Context,这个Context在父Context的Done被关闭时关闭自己的Done通道,或者在自己被Cancel的时候关闭自己的Done。
WithXXX同时还返回一个取消函数cancel,这个cancel用于取消当前的Context。
context简单使用示例
import (
"context"
"fmt"
"time"
)
func main() {
//创建一个context根节点
root := context.Background()
//根据根节点派生出子节点
ctx,cancel := context.WithCancel(root)
//开启协程
go test(ctx)
//十秒后取消子节点context
time.Sleep(time.Second * 10)
cancel()
time.Sleep(time.Second * 1)
}
//将context作为参数传入函数
func test(ctx context.Context) {
//循环,每秒检测一次当前context是否被取消
for {
time.Sleep(1 * time.Second)
select {
//如果检测到当前context被取消(通道被关闭)
case <-ctx.Done():
//关闭协程
fmt.Println("done")
return
default:
//正常运行
fmt.Println("work")
}
}
}
- 这里涉及到一个select知识点:
for循环select时,如果其中一个case的通道已经关闭,则每次都会执行到这个case(如果chan关闭前,通道内元素已经被读完,接下来所有接收的值都会非阻塞直接成功,返回 chan元素的零值,但是第二个bool值一直为false)。
如果select里边只有一个case,而这个case被关闭了,则会出现死循环。
运行结果
work
work
work
work
work
work
work
work
work
done
Process finished with the exit code 0