一、Context概述
1.缘起
在开发web服务应用时,我们知道http启动的服务每接收到一个请求是便启动一个goroutine处理该request。而每个协程处理该请求是一般都会启动多个协程去处理不同任务,如调用RPC、访问数据库资源、缓存资源等等,这些协程都是为处理同一个request工作的,同时当request被取消或者超时的时候,从这个request处理协程创建的所有子协程也应该被结束。此时一个handler就必须对其启动的子协程有控制权,在context出现前,上述那些处理还是很丑陋的,有些甚至引起全局资源的滥用或者回调噩梦。context出现后,一切都得到解脱,context解决了处理同一生命周期协程树的资源管理问题。
2.官方解释:
Context,翻译为“上下文”,context包定义了Context接口类型,其接口签名方法定义了跨API边界和进程之间的执行最后期限、取消信号和其他请求范围的值。
对服务器的传入请求应创建Context类型,对服务器的传出调用应接受Context。它们之间的函数调用链必须传播Context,可以选择将其替换为使用WithCancel()、WithDeadline()、WithTimeout()或WithValue()创建的派生Context。当一个context被取消时,从它派生的所有context也被取消。WithCancel()、WithDeadline()和WithTimeout()函数接受上下文(父级)并返回派生上下文(子级)和Cancelfunc。调用Cancelfunc将取消子级及其子孙级,删除父级对子级的引用,并停止任何关联的计时器。如果不调用Cancelfunc,则会泄漏子级及其子孙级,直到父级被取消或计时器触发。Go-Vet工具检查取消功能是否用于所有控制流路径。
使用Context的程序应该遵循这些规则,以保持包之间的接口一致,并允许静态分析工具检查上下文传播:
//context传递的写法
func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}
不要将Context存储在结构类型中;而是将Context显式传递给每个需要它的函数。文应该是第一个参数,通常名为ctx:
即使函数允许,也不要传递nil上下文。如果不确定要使用哪个上下文,请传递context.TODO(),该函数返回一个可被跟踪的顶级Context。
只对传输进程和API的请求范围数据使用Context值,而不用于向函数传递可选参数。
同一Context可以传递给在不同goroutine中运行的函数;上下文对于多个goroutine同时使用是安全的。
3.context包解析
我们来看一下Context接口的签名方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
3.1 Context接口签名方法解析:
- Deadline() (deadline time.Time, ok bool)
Deadline方法返回应取消代表此上下文完成的工作的时间。
- 未设置截止时间时,Deadline方法返回ok==false。
- 对Deadline方法的连续调用将返回相同的结果。
- Done() <-chan struct{}
done返回一个通道,该通道在应取消代表此上下文完成的工作时关闭。如果无法取消此上下文,则done可能返回nil。对done的连续调用返回相同的值。不同的派生Context对done通道关闭有不同的处理方式:
- WithCancel()在调用cancel时安排关闭done;
- WithDeadline()在截止时间过期时安排关闭done;
- WithTimeout()在超时结束时安排关闭done。
- Err() error
Err方法在done关闭后返回非零错误值。
返回值:
- 如果上下文被取消,则返回Canceled;如果上下文的截止时间已过,则返回DeadLineExceeded;
- 没有为err定义其他值。
- 完成后关闭,对err的连续调用将返回相同的值。
- Value(key interface{}) interface{}
- 该方法可以让协程共享一些数据,获得数据是协程安全的。
- 该方法返回与键的上下文关联的值,如果没有值与键关联,则返回nil。
- 对具有相同键的值的连续调用返回相同的结果。
仅对传输进程和API边界的请求范围数据使用上下文值,而不用于向函数传递可选参数。
键标识上下文中的特定值。希望在上下文中存储值的函数通常在全局变量中分配一个键,然后使用该键作为context.WithValue() 和 Context.Value的参数。键可以是支持相等的任何类型;包应将键定义为未排序的类型以避免冲突。
3.2 顶级Context
context包提供两种顶级的上下文类型,由工厂方法创建:
(1).func Background() Context
context.Background()返回非零的空上下文。它从不被取消,没有值,也没有最后期限。它通常由主函数、初始化和测试使用,并且作为传入请求的顶级上下文。
(2).func TODO() Context
context.TODO()返回非零的空上下文。当不清楚要使用哪个上下文或者它还不可用时(因为周围的函数还没有被扩展以接受上下文参数),应该使用context.TODO()。静态分析工具可以识别TODO,它确定上下文是否在程序中正确传播。
两者区别:
==本质来讲两者区别不大,其源码实现是一样的,只不过使用场景不同,context.Background()通常由主函数、初始化和测试使用,是顶级Context;context.TODO()通常用于主协程外的其他协程向下传递,分析工具可识别它在调用栈中传播。==
3.3 派生Context
除以上两种顶级Context类型,context包提供四种创建可派生Context类型的函数:
(1). func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel函数返回具有新done通道的父级副本。当调用返回的cancel函数或关闭父上下文的done通道时(以先发生者为准),将关闭返回的上下文的done通道。
取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用Cancel。
官方使用示例:
// gen generates integers in a separate goroutine and
// sends them to the returned channel.
// The callers of gen need to cancel the context once
// they are done consuming generated integers not to leak
// the internal goroutine started by gen.
gen := func(ctx context.Context) <-chan int {
dst := make(chan int)
n := 1
go func() {
for {
select {
case <-ctx.Done():
return // returning not to leak the goroutine
case dst <- n:
n++
}
}
}()
return dst
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // cancel when we are finished consuming integers
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
break
}
}
//OUTPUT:
1
2
3
4
5
(2). func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
WithDeadline函数返回父上下文的副本,其截止时间调整为不迟于d。如果父上下文的截止时间早于d,则WithDeadline(Parent,d)在语义上等同于父上下文。当截止时间到期、调用返回的cancel函数或关闭父上下文的done通道(以先发生者为准)时,返回的上下文的done通道将关闭。
取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用Cancel。
官方使用示例:
这个例子传递一个具有任意截止时间的上下文,告诉一个阻塞函数一旦到达它就应该放弃它的工作。
d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err())
}
//OUTPUT:
context deadline exceeded
(3). func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。取消此上下文将释放与其关联的资源,因此代码应在此上下文中运行的操作完成后立即调用取消:
官方使用示例:这个例子传递一个带有超时的上下文,告诉一个阻塞函数它应该在超时结束后放弃它的工作。
// Pass a context with a timeout to tell a blocking function that it
// should abandon its work after the timeout elapses.
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
select {
case <-time.After(1 * time.Second):
fmt.Println("overslept")
case <-ctx.Done():
fmt.Println(ctx.Err()) // prints "context deadline exceeded"
}
//OUTPUT:
context deadline exceeded
*以上函数的特殊返回类型值:type CancelFunc func()
CancelFunc告诉操作放弃其工作。CancelFunc不等待工作停止。在第一次调用之后,对CancelFunc的后续调用不做任何操作。
(4). func WithValue(parent Context, key, val interface{}) Context
WithValue返回父级的副本,可为上下文设置一个键值对。
只对传输进程和API的请求范围数据使用上下文值,而不用于向函数传递可选参数。
提供的键必须是可比较的,并且不应是字符串或任何其他内置类型,以避免使用上下文的包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给接口时进行分配,上下文键通常具有具体的类型结构。或者,导出的上下文键变量的静态类型应该是指针或接口。
官方使用示例:
type favContextKey string
f := func(ctx context.Context, k favContextKey) {
if v := ctx.Value(k); v != nil {
fmt.Println("found value:", v)
return
}
fmt.Println("key not found:", k)
}
k := favContextKey("language")
ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k)
f(ctx, favContextKey("color"))
//OUTPUT:
found value: Go
key not found: color
三、Context使用示例
使用context
包来实现线程安全退出或超时的控制:
//定义一个并发worker
func worker(ctx context.Context, wg *sync.WaitGroup) error {
defer wg.Done()
for {
select {
//当父协程调用cancel()时,会从ctx.Done()得到struct{},此时返回ctx.Err()退出子线程
case <-ctx.Done():
return ctx.Err()
default:
//默认输出hello
fmt.Println("hello")
}
}
}
func main() {
//生成一个有超时控制的衍生Context,超时10s退出所有子协程
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go worker(ctx, &wg)
}
//主协程1s后就cancel所有子协程了,每个worker都可以安全退出
time.Sleep(time.Second)
cancel()
wg.Wait()
}
当并发体超时或main
主动停止工作者Goroutine时,每个工作者都可以安全退出。
Go语言是带内存自动回收特性的,因此内存一般不会泄漏。当main
函数不再使用管道时后台Goroutine有泄漏的风险。我们可以通过context
包来避免这个问题,下面是防止内存泄露的素数筛实现:
// 返回生成自然数序列的管道: 2, 3, 4, ...
func GenerateNatural(ctx context.Context) chan int {
ch := make(chan int)
go func() {
for i := 2; ; i++ {
select {
//父协程cancel()时安全退出该子协程
case <- ctx.Done():
return
//生成的素数发送到管道
case ch <- i:
}
}
}()
return ch
}
// 管道过滤器: 删除能被素数整除的数
func PrimeFilter(ctx context.Context, in <-chan int, prime int) chan int {
out := make(chan int)
go func() {
for {
if i := <-in; i%prime != 0 {
select {
//父协程cancel()时安全退出该子协程
case <- ctx.Done():
return
case out <- i:
}
}
}
}()
return out
}
func main() {
// 使用一个可由父协程控制子协程安全退出的Context。
ctx, cancel := context.WithCancel(context.Background())
ch := GenerateNatural(ctx) // 自然数序列: 2, 3, 4, ...
for i := 0; i < 100; i++ {
// 新出现的素数打印出来
prime := <-ch
fmt.Printf("%v: %v\n", i+1, prime)
// 基于新素数构造的过滤器
ch = PrimeFilter(ctx, ch, prime)
}
//输出100以内符合要求的素数后安全退出所有子协程
cancel()
}
当main函数完成工作前,通过调用cancel()
来通知后台Goroutine退出,这样就避免了Goroutine的泄漏。