为什么需要 Context:在并发程序中,由于超时、取消操作或者一些异常情况,往往需要进行抢占操作或者中断后续操作
1.举个例子:在 Go http 包的 Server 中,每一个请求都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和 RPC 服务,用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速中断退出,然后系统才能释放这些 goroutine 占用的资源
说白了就是一个程序可能在主 goroutine 起了一个 子goroutine 来启动对应的服务,这个 子goroutine 里面可能又存在一个 孙goroutine 来进行其他动作,那么如果我现在想取消在主 goroutine 中产生的所有子 goroutine 或者孙 goroutine,那这时就可以使用 context
context 的常用场景
1. 上下文控制
2. 一个请求对应多个 goroutine 之间的数据交互
3. 超时控制
Context 实际上是一个接口,其提供了四个方法
//Deadline 返回 ctx 的截止时间,ok 为 false 表示没有设置。
//达到截止时间的 ctx 会被自动 Cancel 掉;
Deadline() (deadline time.Time, ok bool)
//如果当前 ctx 是可取消的,Done 返回一个chan 用来监听,否则返回 nil。
//当ctx被Cancel时,返回的chan会同时被 close 掉,也就变成“有信号”状态;
Done() <-chan struct{}
//如果 ctx 已经被 canceled,Err 会返回执行 Cancel 时指定的error,否则返回nil;
//也就是返回 Context 被取消的原因
Err() error
//Value 用来从 ctx 中根据指定的 key 提取对应的 value
Value(key interface{}) interface{}
Context 提供了四种 context,分别是普通 context, 可取消的 context, 超时 context 以及带值的 context
总结为一个接口,四种实现,六个函数
BackGround() 是所有 Context 的 root,不能被 cancel
// 普通context,通常这样调用: ctx, cancel := context.WithCancel(context.Background())
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 带超时的context,超时之后会自动close对象的Done,与调用CancelFunc的效果一样
// WithDeadline 明确地设置一个指定的系统时钟时间,如果超过就触发超时
// WithTimeout 设置一个相对的超时时间,也就是deadline设为timeout加上当前的系统时间
// 因为两者事实上都依赖于系统时钟,所以可能存在微小的误差,所以官方不推荐把超时间隔设置得太小
// 通常这样调用:ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 带有值的context,没有CancelFunc,所以它只用于值的多goroutine传递和共享
// 通常这样调用:ctx := context.WithValue(context.Background(), "key", myValue)
func WithValue(parent Context, key, val interface{}) Context
应用场景1:上下文控制
func worker(ctx context.Context, name string) {
go func() {
for {
select {
case <-ctx.Done():
fmt.Println(name, "got the stop channel")
return
default:
fmt.Println("still working")
time.Sleep(time.Second)
}
}
}()
}
func main(){
//context.Background() 返回一个空的 Context, 这个空的 Context 一般用于
//整个 Context 树根节点。然后使用这个 context.WithCancel(parent) 函数
//创建一个可取消的子 Context,再当作参数传递给 goroutine 使用,这样就可以使用这个
//子 Context 跟踪这个 goroutine
ctx, cancel := context.WithCancel(context.Background()) //返回根节点
//开启多个 goroutine,传入 ctx
go worker(ctx, "node01")
go worker(ctx, "node02")
go worker(ctx, "node03")
time.Sleep(2 * time.Second)
fmt.Println("stop the goroutine")
//停止掉所有的 goroutine,包括根节点及其下面的所有子节点(这个相当于一个总控开关)
cancel()
time.Sleep(2 * time.Second)
}
// Handler for example request
func exampleHandler(w http.ResponseWriter, req *http.Request) {
fmt.Println("example handler started")
// Accessing the context of the request
context := req.Context()
select {
//这里等待十秒钟模拟服务端对客户端请求的处理动作
case <-time.After(10 * time.Second):
fmt.Fprintf(w, "example\n")
//处理客户端的请求取消动作
case <-context.Done():
err := context.Err()
fmt.Println("server:", err)
}
fmt.Println("example handler ended")
}
func main() {
http.HandleFunc("/example", exampleHandler)
http.ListenAndServe(":5000", nil)
}
curl localhost:/example 等待十秒会正常输出"example",但是如果中途客户端停止访问,
则会返回一个context.Err的错误
应用场景2:一个请求对应多个 goroutine 之间的数据交互
var key string = "name"
func run(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Printf("任务%v结束退出\n", ctx.Value(key))
return
default:
fmt.Printf("任务%v正在运行中\n", ctx.Value(key))
time.Sleep(time.Second * 2)
}
}
}
func main() {
//管理启动的协程
ctx, cancel := context.WithCancel(context.Background())
// 给ctx绑定键值,传递给 goroutine, 这个 key-value 就是 "name"-"监控"
valueCtx := context.WithValue(ctx, key, "监控")
// 开启goroutine,传入ctx
go run(valueCtx)
// 运行一段时间后停止
time.Sleep(time.Second * 10)
fmt.Println("停止任务")
//使用context的cancel函数停止goroutine,从上面这几个例子可以看出,
//cancel()起作用的方式其实就是关闭 context's Done channel
cancel()
// 为了检测监控过是否停止,如果没有监控输出,表示停止
time.Sleep(time.Second * 3)
}
func main() {
router := mux.NewRouter()
router.Use(guidMiddleware)
router.HandleFunc("/ishealthy", handleIsHealthy).Methods(http.MethodGet)
http.ListenAndServe(":8080", router)
}
func handleIsHealthy(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
uuid := r.Context().Value("uuid") //这里使用ctx注入的key-value值
log.Printf("[%v] Returning 200 - Healthy", uuid)
w.Write([]byte("Healthy"))
}
func guidMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uuid := uuid.New()
//这里注入context的一个key-value值
r = r.WithContext(context.WithValue(r.Context(), "uuid", uuid))
next.ServeHTTP(w, r)
})
}
应用场景3:超时控制
WithTimeout 实际上调用的还是 WithDeadline 函数,WithTimeout 会在指定的时间后自动关闭掉 Ctx.Done这个channel,但是观察这个函数的返回值可以发现其还是返回了一个cancel函数,这个cancel 函数是提供给用户手动关闭Ctx.Done这个channel的
func coroutine(ctx context.Context, duration time.Duration, id int, wg *sync.WaitGroup) {
for {
select {
case <-ctx.Done():
fmt.Printf("协程 %d 退出\n", id)
wg.Done()
return
case <-time.After(duration):
fmt.Printf("消息来自协程 %d\n", id)
}
}
}
func main() {
//使用 WaitGroup 等待所有的goroutine执行完毕,在收到<-ctx.Done()的终止信号后使wg中需要
//等待的 goroutine 数量减一,因为 context 只负责取消 goroutine,不负责等待 goroutine 运行,
//所以需要配合一点辅助手段管理启动的协程
wg := &sync.WaitGroup{}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() //官方建议这里写一个 defer cancel()
for i := 0; i < 3; i++ {
wg.Add(1)
go coroutine(ctx, 1*time.Second, i, wg)
}
wg.Wait()
}