Context的主要作用:
1.上下文信息传递 ,比如处理 http 请求、在链路追踪中传递信息;
2.控制子 goroutine 的运行;
3.超时控制的方法调用;
4.可以取消的方法调用。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
- Done
会返回一个channel,当该context被取消的时候,该channel会被关闭,同时对应的使用该context的routine也应该结束并返回。
- Context
中的方法是协程安全的,这也就代表了在父routine中创建的context,可以传递给任意数量的routine并让他们同时访问。
- Deadline
会返回一个超时时间,routine获得了超时时间后,可以对某些io操作设定超时时间。
- Value
可以让routine共享一些数据,当然获得数据是协程安全的。
接下来,我会介绍标准库中几种创建特殊用途 Context 的方法:WithValue、WithCancel、WithTimeout 和 WithDeadline,包括它们的功能以及实现方式。要创建context树,第一步是要有一个根结点。context.Background
函数的返回值是一个空的context,经常作为树的根结点,它一般由接收请求的第一个routine创建,不能被取消、没有值、也没有过期时间。
func Background() Context
之后该怎么创建其它的子孙节点呢?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 interface{}, val interface{}) Context
WithCancel
,当顶层的Request请求处理结束,或者外部取消了这次请求,就可以cancel掉顶层context,从而使整个请求的routine树得以退出。不是只有你想中途放弃才去调用 cancel,而是只要你的任务正常完成了,就需要调用 cancel,这样,这个 Context 才能释放它的资源。
WithDeadline
和WithTimeout
比WithCancel
多了一个时间参数,它指示context存活的最长时间。如果超过了过期时间,会自动撤销它的子context。所以context的生命期是由父context的routine和deadline
共同决定的。
WithValue
返回parent的一个副本,该副本保存了传入的key/value,而调用Context接口的Value(key)方法就可以得到val。注意在同一个context中设置key/value,若key相同,值会被覆盖。
package main
import (
"context"
"fmt"
)
func func1(ctx context.Context) {
ctx = context.WithValue(ctx, "k1", "v1")
func2(ctx)
}
func func2(ctx context.Context) {
fmt.Println(ctx.Value("k1").(string))
}
func main() {
ctx := context.Background()
func1(ctx)
}
package main
import (
"context"
"errors"
"fmt"
"sync"
"time"
)
func func1(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
respC := make(chan int)
// 处理逻辑
go func() {
time.Sleep(time.Second * 5)
respC <- 10
}()
// 取消机制
select {
case <-ctx.Done():
fmt.Println("cancel")
return errors.New("cancel")
case r := <-respC:
fmt.Println(r)
return nil
}
}
func main() {
wg := new(sync.WaitGroup)
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go func1(ctx, wg)
time.Sleep(time.Second * 2)
// 触发取消
cancel()
// 等待goroutine退出
wg.Wait()
}
package main
import (
"context"
"fmt"
"time"
)
func func1(ctx context.Context) {
hctx, hcancel := context.WithTimeout(ctx, time.Second*4)
defer hcancel()
resp := make(chan struct{}, 1)
// 处理逻辑
go func() {
// 处理耗时
time.Sleep(time.Second * 10)
resp <- struct{}{}
}()
// 超时机制
select {
// case <-ctx.Done():
// fmt.Println("ctx timeout")
// fmt.Println(ctx.Err())
case <-hctx.Done():
fmt.Println("hctx timeout")
fmt.Println(hctx.Err())
case v := <-resp:
fmt.Println("test2 function handle done")
fmt.Printf("result: %v\n", v)
}
fmt.Println("test2 finish")
return
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*2)
defer cancel()
func1(ctx)
}
在使用 Context 的时候,有一些约定俗成的规则。
1.一般函数使用 Context 的时候,会把这个参数放在第一个参数的位置。
2.从来不把 nil 当做 Context 类型的参数值,可以使用 context.Background() 创建一个空的上下文对象,也不要使用 nil。
3.Context 只用来临时做函数之间的上下文透传,不能持久化 Context 或者把 Context 长久保存。把 Context 持久化到数据库、本地文件或者全局变量、缓存中都是错误的用法。
4.key 的类型不应该是字符串类型或者其它内建类型,否则容易在包之间使用 Context 时候产生冲突。使用 WithValue 时,key 的类型应该是自己定义的类型。
5.常常使用 struct{}作为底层类型定义 key 的类型。对于 exported key 的静态类型,常常是接口或者指针。这样可以尽量减少内存分配。
目前收集的关于Context存在的问题主要有:
1.Context 包名导致使用的时候重复 ctx context.Context;
2.Context.WithValue 可以接受任何类型的值,非类型安全;
3.Context 包名容易误导人,实际上,Context 最主要的功能是取消 goroutine 的执行;
4.Context 漫天飞,函数污染。
Go Concurrency Patterns: Context:https://go.dev/blog/context
Go: Context and Cancellation by Propagation:https://medium.com/a-journey-with-go/go-context-and-cancellation-by-propagation-7a808bbc889c
剖析 Golang Context:从使用场景到源码分析:https://xie.infoq.cn/article/3e18dd6d335d1a6ab552a88e8
Go Context的踩坑经历:https://zhuanlan.zhihu.com/p/34417106?hmsr=toutiao.io
Go官方文档:https://pkg.go.dev/context
极客时间go并发编程实战课