1. Golang Context简介
在Go语言中,context
是一个在Go语言程序中传递截止时间、取消信号以及其它请求作用域的值的包。context
包为并发操作提供了一种统一的方式来传递取消信号和元数据。经常用于控制不同Go协程之间的超时和取消操作,管理函数或方法的生命周期。
参考文档:上下文Context
2. Context用途
context
的主要用途有以下几点:
1、超时控制:通过在 context
中设置超时来控制任务的执行时长,例如 HTTP 请求或数据库查询,以防止一些操作耗时过长影响系统性能。
2、取消操作:在多个协程之间同步取消信号,如一个用户请求的取消操作需要传递给所有相关的处理协程。
3、传递请求相关的数据:虽然不是 context
的主要用途,但它也可用于在API的调用链中传递请求特定的数据,如身份验证令牌等。
3. Context关键概念
3.1. Context类型
Context 类型: context
包中定义了 Context
接口,它包括以下方法:
Deadline()
:返回context
截止时间。Done()
:返回一个channel
,context
取消或者达截止时间时,channel
会被关闭。Err()
:返回context
结束的错误,如果context
被取消就返回Canceled
错误,如果超时则返回DeadlineExceeded
错误。Value(key interface{})
:获取与key
对应的值,用于传递请求特定的数据。
3.2. Context对象
创建 Context 对象:
context.Background()
:返回一个空的Context
,通常用于整个Context
树的根节点。它不可以取消,也没有截止时间。我们并不需要,也不能主动去关闭它。它通常是用在main函数或顶层的初始化函数中,提供了一个基础的 Context,该Context通常被扩展为更复杂的 Context。context.TODO()
:返回一个空的Context
,用于占位。通常在代码的某个部分还没有决定应该使用哪个 Context 时使用,或者在当前没有可用的 Context,但我们预计将来某个时间点需要向下传递一个实际的 Context 时使用。context.WithCancel(parent Context)
:基于父Context
创建新的可以主动取消的Context
。context.WithDeadline(parent Context, deadline time.Time)
:基于父Context
创建新的带有截止时间的Context
,当达到截止时间时,Context
会自动取消,也可以主动取消。context.WithTimeout(parent Context, timeout time.Duration)
:基于父Context
和超时时间创建新的Context
。
3.3. Context树
Context
树是指在Go的context
包中通过Context
对象的派生形成的层次结构。在这个层次结构中,每个Context
对象都可能有父Context
,同时也可以有一个或多个子Context
。这种结构类似于一颗树,其中最顶层的Context
通常是context.Background()
或context.TODO()
,它们通常作为所有Context
层次结构的根节点。
创建子Context
的过程一般通过context.WithCancel
、context.WithDeadline
、context.WithTimeout
和context.WithValue
四个函数实现。每个函数在创建子Context
时都会复制父Context
的行为(诸如超时、取消信号等),同时可以增加额外的行为或信息。
例如,如果我们有一个HTTP请求的处理逻辑,我们可能会在这个请求的开始处创建一个根Context
(通过context.WithTimeout
来设置整个请求的超时),然后随着我们的代码调用更深层的函数,或者启动新的goroutine来处理请求的不同部分,我们会从根Context
派生子Context
。
具体来说,如果一个数据库查询依赖于这个请求,我们可以再从这个根Context
派生一个子Context
(可能还会设置一个特定于数据库操作的超时时间),并将其传递给执行数据库操作的函数。如果需要取消整个请求处理,只需要取消最顶层的Context
,整个Context
树下所有的Context
都会收到取消信号。
以下展示了Context
树的简单结构示意:
1 | Root Context (Background or TODO) |
在这个结构中:
- 取消
Root Context
会导致整个树的Context
都被取消。 - 取消
Child Context A
只会影响A
的子树,即Grandchild Context A.1
与Grandchild Context A.2
,不会影响Child Context B
子树。 - 子
Context
会继承父Context
的截止时间和取消信号,并可以拓展自己的值与超时设置。
在并发编程和复杂的应用逻辑中,管理Context
树可以帮助开发者更有效地处理超时、取消操作以及传递关键的请求信息。
4. Context最佳实践
- 不要将
Context
存放在结构体中,而应明确地传递。 - 务必传递而非忽略
Context
,即使代码还不需要它。 - 即使函数允许,也不要传递
nil
Context
,如果不确定使用哪个Context
,请使用context.TODO()
。 Context
的Value
应该用得极其谨慎,它不是传递可选参数的手段。
一个典型的使用场景是在服务器处理请求时,使用 context.WithCancel
或 context.WithTimeout
在入口处产生一个 Context
,然后将这个 Context
传递给每个涉及到的协程,让这些并发执行的操作都可以响应取消或超时事件。这样、单一请求的依赖操作可以被统一管理和控制。
5. Context使用示例
5.1. 创建可取消的Context
1 | package main |
在这个例子中,我们通过context.WithCancel
创建了一个可取消的Context。在operation
函数中,我们使用select
监听Context的Done
通道是否关闭。一秒后,执行cancel()
函数,此时Done
通道关闭,程序输出”Operation canceled”,而不会等到5秒的超时时间。
5.2. 创建带超时的Context
1 | package main |
在这个例子中,我们通过context.WithTimeout
创建了一个带有2秒超时的Context。由于operation
函数中的模拟操作需要5秒才能完成,Context会在2秒后因超时而取消。<-ctx.Done()
等待Context的取消信号,打印出超时信息。
5.3. Context传递值
1 | package main |
在这个例子中,我们使用context.WithValue
向context
添加了一个键值对。然后在doSomething
函数中,我们从context
中检索值并打印它。需要注意的是,如前所述,WithValue
应该非常谨慎地使用,因为它不适合传递很多值,并且可能会导致依赖于context的代码变得难以理解和维护。
在实际应用中,context
常常用于API调用链、数据库操作、HTTP请求处理等多级协程控制场景中。使用context
能够优雅地管理多个协程间的超时、取消和信号传递。
5.4. Context传递停止信号
ctx.Done()
返回只读chan类型的通道关闭信号。<-ctx.Done()
用于等待ctx.Done方法返回的通道关闭信号。
当context
被取消或超时的情况下,Done
通道会被关闭,这表示相关的操作应该停止执行。
我们可以通过监听这个Done
通道来优雅地处理取消信号和超时事件。我们通常会在长时间运行或涉及到并发的操作中使用这种方法。
Done()用法的基本示例:
1 | package main |
在这个例子中,我们通过ctx.Done()
来监控一个长时间运行的任务是否应该停止。
当Context的超时时间到了(在本例中是3秒后),Done通道会关闭,同时select
语句的case <-ctx.Done()
分支被选中,程序打印出取消或超时的信息,并且退出for
循环,结束任务。
在主函数(main
)中,我们也监听了<-ctx.Done()
,以便等待Context的取消信号,进行必要的清理工作。
这种做法使得并发程序在需要响应停止指令(如取消或超时)时能够及时安全地停止正在执行的操作。
5.5. 多个子任务并发执行
下面是多个子函数并发执行的示例,每个子函数都通过监听<-ctx.Done()
来响应取消信号:
1 | package main |
在这个程序中:
1、我们首先使用 context.WithCancel(context.Background())
创建了一个可取消的父 Context
,这个 Context
会被作为参数传给所有的任务(子函数)。
2、我们使用 sync.WaitGroup
确保主函数等待三个并发任务全部完成。每个任务的goroutine在开始时调用 wg.Add(1)
增加等待计数,当任务完成时调用 wg.Done()
递减计数。
3、task
函数表示需要执行的任务。它通过 select
语句监听两个事件:ctx.Done()
信号的关闭,表示任务应该取消;default
分支模拟正在进行的工作(这里简化为打印信息和等待1秒)。
4、在 main
函数中,我们使用 time.AfterFunc
设置了一个3秒后执行的定时器,在定时器触发时调用 cancel()
函数取消所有任务。
5、最后,wg.Wait()
阻塞主函数,直到所有goroutine执行 wg.Done()
,这也就意味着所有任务都接收到取消信号并正常退出。
在上述示例中,当主函数中的定时器触发,所有任务都会因 ctx.Done()
的关闭而接收到取消信号并优雅地停止正在进行的工作。程序将输出”Task x is canceled”信息并退出。所有任务取消后,程序打印 “All tasks have been cancelled.”,然后结束。
6. 总结
Context的本质作用,其实是控制函数或方法的生命周期。
Context生命周期控制机制有两个关键概念:一个是ctx对象,一个是ctx对象的channel。
Context生命周期控制的流程可以概括为:
1、父函数中,创建一个ctx对象,并且把该对象传递给子函数
2、父函数中,可以通过定时或者手动取消,关闭ctx对象的channel
3、父函数中关闭ctx对象的channel时,子函数就收到了关闭信号,子函数可以选择结束自己的生命周期
4、如果子函数长时间运行,或者多个子函数并发,父函数中可以使用 <-ctx.Done()
使主函数进入等待子函数执行
PS:多个子函数并发,还可以使用 sync.WaitGroup
等待子函数执行