标准库 context Q&A
参考文档
context如何被取消
1 . context .Context 讲解
type Context interface {
Deadline () (deadline time.Time, ok bool)
Done () <-chan struct{}
Err () error
Value (key interface{}) interface{}
}
2 . Context是一个接口,定义了4 个方法,它们都是幂等的,也就是说连续多次调用同一个方法,得到的结果是相同的
3 . Done ()返回一个channel, 可以表示context被取消的信号,当这个channel被关闭了,说明context被取消了
注意这里是一个只读的channel, 我们知道读取一个已关闭的channel,会读出响应类型的零值,并且源码里没有地方
会向这个channel塞入值,换句话说,这个一个read-only的channel,因此在子协程里读取这个channel,
除非被关闭,否则读取不出来任何东西, 也正是利用了这一点,子协程从channel中读出值(零值)后,就可以收尾退出了
4 . Err () 返回一个错误,表示channel被关闭的原因,例如是被取消,还是超时
5 . Deadline () 返回context的截止时间
6 . Value () 获取之前设置的key对应的value
7 . context.Background ()通常用在main 函数中,作为所有context的根节点
context.TODO ()通常用在字并不知道传递什么context的情形,例如调用一个需要传递context的函数,
你手头并没有其它context可以传递,这时就可以传递TODO (),这常常发生在重构中,给一些函数添加了一个Context参数
但不知道要传递什么,就用TODO占个位子,最终要换成其他context
context是什么
1 . go1.7 标准库引入context ,中文名就是"上下文" , 准确说它是goroutine的上下文,包含goroutine的运行状态、环境、现场等信息
main goroutine 通知 child goroutine退出任务的案例
func main () {
messages := make (chan int , 10 )
done := make (chan bool )
go func () {
var ticker = time.NewTicker(time.Second)
for _ = range ticker.C{
select {
case <-done:
fmt.Println("main goroutine 通知 child goroutine结束了..." )
return
default :
fmt.Println("messages: " , <-messages)
}
}
}()
for i := 0 ; i < 10 ; i++{
messages <- i
}
time.Sleep(time.Second * 5 )
close (done)
time.Sleep(time.Second)
fmt.Println("main goroutine 结束了..." )
}
2 . 上述例子中定义了一个buffer为0的channel done, 子协程运行这定时任务,如果主协程需要在某个时刻发送消息通知
子协程中断任务并退出,那么就可以让子协程监听这个done channel, 一旦主协程关闭done channel,那么子协程就可以
退出了,这样实现了主协程通知子协程的需求,但是还是有限的
3 . 假如我们可以在简单的通知上附加额外的信息来控制取消,为什么取消,或者有一个它必须要完成的最终期限
更或者有多个取消选项,我们需要额外的信息来判断选择执行哪个取消选项
4 . 考虑下面这种情况,假如主协程有多个任务,1 ,2 ,m,主协程对这些任务有超时控制,
而其中任务1 又有多个子任务1 ,2 ,n, 任务1 对这些子任务也有超时控制,
那么这些子任务即要感知主协程的取消信号,也要感知任务1 的取消信号,
5 . 如果还是使用done channel的方法,我们需要定义两个done channel, 子任务需要监听这两个done channel
这样好像也还行,但是如果层级更深的话,这些子任务还有子任务的话,那么使用done channel的方法将变得非常繁琐且混乱
6 . 我们需要优雅的方案来实现这一种机制:
* 上层任务取消后,所有的下层任务都会被取消
* 中间某一层任务取消后,只会将当前任务的下层任务全部取消,而不会影响上层任务及同级任务
这个时候,context 就派上用场了
7 . Context 接口包含四个方法:
Deadline返回绑定当前context 的任务被取消的截止时间;如果没有设定期限,将返回ok == false。
Done 当绑定当前context 的任务被取消时,将返回一个关闭的channel;如果当前context 不会被取消,将返回nil。
Err 如果Done返回的channel没有关闭,将返回nil
Value 返回context 存储的键值对中当前key对应的值,如果没有对应的key,则返回nil。
8 . 可以看到Done()方法返回的channel用来传递结束信号以中断当前任务,
Deadline()方法指示一段时间后当前goroutine是否会被取消,
以及一个Err()方法用来解释goroutine被取消的原因,而Value用于获取当前任务树的额外信息,
9 . Background()方法和TODO()方法生成的context其实是一致的,那么我们何时调用哪个呢
background通常用于主函数、初始化、以及测试中使用,作为一个顶层的context,
也就是说一般我们创建的context 都是基于bakground
TODO()是在不确定使用什么context 的时候使用
两种不同功能的基础context类型,valueCtx、cancelCtx
type valueCtx struct {
Context
key, val interface {}
}
func (c * valueCtx) Value(key interface {}) interface {} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
1 . valueCtx嵌入了Context 变量来表示父节点context ,表示当前context 继承了父context 的所有信息
valueCtx还携带了一组键值对,也就是说这种context 可以携带额外的信息,valueCtx实现了Value方法
用以在context 链路上获取key对应的值,如果当前context 不存在需要的key,会沿着context 链向上
寻找key对应的值,直到根节点
WithValue
1. WithValue用以向context添加键值对,
func WithValue (parent Context, key, val interface {}) Context {
if parent == nil {
panic ("cannot create context from nil parent" )
}
if key == nil {
panic ("nil key" )
}
if !reflectlite.TypeOf(key).Comparable() {
panic ("key is not comparable" )
}
return &valueCtx{parent, key, val}
}
2 . 这里添加键值对不是在原结构体上直接添加,而是以此context 作为父节点,重新黄建一个valueCtx子节点
将键值对添加到子节点上,由此形成一条context 链,获取value的过程就是在此context 链上由尾部向前搜索
cancelCtx
type cancelCtx struct {
Context
mu sync.Mutex
done atomic.Value
children map [canceler]struct {}
err error
}
1 . cancelCtx跟valueCtx类似,cancelCtx结构体中也有一个变量context 作为父节点,
变量done表示一个channel, 用来表示传递关闭信号,children表示一个map ,用来存储当前context 节点下的子节点
err存储错误信息表示被取消的原因
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
// 设置取消原因
c.err = err
d, _ := c.done.Load().(chan struct{})
// 设置一个关闭的channel或者将done channel关闭,用以发送关闭的信号
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
// 将子节点context依次取消
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
// 将当前context节点从父节点上移除
if removeFromParent {
removeChild(c.Context, c)
}
}
2 . 可以发现cancelCtx类型的变量其实也是canceler类型,因为cancelCtx实现了canceler接口,
Done ()方法返回的是通知goroutine取消的信号通道,Err ()方法返回的是被取消的原因
cancelCtx类型的context在调用cancel方法时,会设置取消原因,将done channel设置为一个关闭的channel
或者关闭channel, 然后将子节点context依次取消,如果有需要还会将当前节点从父节点直接移除
WithCancel
1. WithCancel函数用来创建一个可取消的context, 即cancelCtx类型的context,
WithCancel()方法返回一个cancelCtx和CancelFunc, 调用CancelFunc即可触发cancel操作,看源码
type CancelFunc func ()
func WithCancel (parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic ("cannot create context from nil parent" )
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func () { c.cancel(true , Canceled) }
}
TimerCtx
1. timerCtx是一种基于cancelCtx的context类型,从字面上就可以看出,这是一种可以定时取消的context类型
timerCtx增加了两个字段,timer和deadline, timer:计时器,deadline:截止日期
2. timerCtx内部使用cancelCtx实现取消,另外使用定时器timer和过期时间deadline实现定时取消的功能,
timerCtx在调用cancel方法时,会先将内部的cancelCtx取消,如果需要则将自己从cancelCtx祖先节点上移除
最后取消计时器
WithDeadline
1 . 如果父节点parent有过期时间,并且过期时间<设置的时间d,那么新建的子节点context 无须设置过期时间
使用WithCancel创建一个可取消的context 即可
2 . 否则就利用parent和过期时间d创建一个定时取消的timerCtx, 并建立context 与可取消context 祖先节点的
取消关联关系,接下来判断当前时间具体过期时间d的时长dur
3 . 如果dur<0 ,表明当前已经过了过期,则直接取消新的timerCtx
4 . 为新建的timerCtx设置定时器,一旦达到过期时间就取消当前的timerCtx
func WithDeadline (parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic ("cannot create context from nil parent" )
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true , DeadlineExceeded)
return c, func () { c.cancel(false , Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func () {
c.cancel(true , DeadlineExceeded)
})
}
return c, func () { c.cancel(true , Canceled) }
}
WithTimeout
func WithTimeout (parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline (parent, time.Now().Add (timeout))
}
1. 与WithDeadline类似,WithTimeout也是创建一个定时取消的context,只不过WithDeadline是接收一个过期时间点
而WithTimeout是接收一个过期时长,看源码也知道,WithTimeout也是调用的WithDeadline
Context的使用
1. 使用context实现文章开头done channel的例子示范一下如何更优雅的实现协程间取消信号的同步
func main () {
messages := make (chan int , 10 )
for i := 0 ; i < 10 ; i++{
messages <- i
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second * 5 )
go func (ctx context.Context) {
ticker := time.NewTicker(time.Second)
for _ = range ticker.C{
select {
case <-ctx.Done():
fmt.Println("child goroutine 结束了..." )
return
default :
fmt.Println("receive message: " , <-messages)
}
}
}(ctx)
defer close (messages)
defer cancel()
select {
case <-ctx.Done():
time.Sleep(time.Second)
fmt.Println("main goroutine 结束了" )
}
}
2. 这个例子中,只要让子协程监听主协程传入的ctx,一旦ctx.Done()方法返回空channel, 子线程即可取消执行任务
但是这个例子还无法展现context的传递取消信息的强大优势
3. net/http包的源码里在实现http server就用到了context, 简单分析下
4. 重点:
这样处理的目的主要有以下几点:
1. 一旦请求超时,即可中断当前请求;
2. 在处理构建response过程中如果发生错误,可直接调用response对象的cancelCtx方法结束当前请求;
3. 在处理构建response完成之后,调用response对象的cancelCtx方法结束当前请求。
* 在整个server处理流程中,使用了一条context链贯穿Server、Connection、Request,不仅将上游的信息共享给下游任务,
* 同时实现了上游可发送取消信号取消所有下游任务,而下游任务自行取消不会影响上游任务。
context是什么?
1 . context 主要用来在goroutine之间传递上下文信息,包括取消信号,超时时间,截止时间,k-v等
2 . 随着context 包的引入,标准库中很多接口也增加了context 参数,例如database/sql,
context 几乎成为了并发控制和超时控制的标准做法
context有什么用?
1 . go通常用来写后台服务,只需要几行代码就可以写一个http server,在go的server里
通常每来一个请求都会启动若干个goroutine同时工作,有些去数据库拿数据,有些调用下游接口获取相关数据
这些goroutine需要共享这个请求的基本数据,例如登录的token,处理请求的最大超时时间
当请求被取消或者超时,所有为这个请求工作的goroutine需要快速退出,系统就可以回收相关资源
2 . go语言中的server实际上是一个"协程模型" , 也就是说一个协程处理一个请求,例如业务高峰期,某个下游服务器响应变慢
而当前系统的请求又没有超时控制,或者说超时时间设置的过大,那么等待下游服务器返回数据的协程会越来越多,
协程也是需要消耗资源的,如果携程数量激增,内存暴涨,甚至导致服务不可用,更严重会导致雪崩效应,整个服务对外不可用,P0级别的事故
3 . 上面说的P0级别事故,可以通过设置下游服务器最大处理时间就可以避免,给下游服务器设置timeout=5 ms,
如果这个时间没有接收到数据,就直接返回给客户端一个默认值或错误,
4 . context 包就是为了解决上面这些问题而开发的,在一组goroutine之间传递共享的值,取消信号,超时控制,截止日期
5 . 用简练的话来说,在go里面,我们不能直接杀死协程,需要通过channel+select 的方法来关闭协程,
但是在某些场景下,例如一个请求衍生了很多个协程,这些协程间是相互关联的,需要共享一些全局变量,有共同的deadline
而且可以同时被关闭,再用channel+select 就会比较麻烦,这是就可以通过context 来实现
* 一句话解决:context 用来解决goroutine之间 退出通知、元数据传递 的功能
6 . 【引申1 】举例说明context 在实际项目中如何使用
context 会在函数传递间传递,只需要在适当的时间调用cancel函数就可以向goroutine发出取消信号
或者调用Value函数取出context 中的值
7 . context 使用注意4 个事项
1 . 不要将context 塞入结构体里,直接将context 作为函数的第一参数,而且一般命名为ctx
2 . 不要向函数传入一个nil context ,如果你是在不知道传递什么,标准库给提供好了一个context :todo
3 . 不要把本应该作为函数参数的类型放入到context 中,context 应该存储一些共同的数据,例如登录的session、cookie等
4 . 同一个context 有可能会被传入到多个goroutine,别担心,context 是并发安全的
context可以传递共享的数据
1. 对于web服务端开发,往往希望将一个请求处理的整个过程串起来,这非常依赖于Thread Local(对于go 可以理解为单个协程所独有的)
变量,go 语言中没有Thread Local这个概念,所以在调用函数是需要传递context
func main () {
bgCtx := context.Background()
process(bgCtx)
vCtx := context.WithValue(bgCtx, "traceId" , "123456" )
process(vCtx)
}
func process (ctx context.Context) {
if traceId, ok := ctx.Value("traceId" ).(string ); ok{
fmt.Printf("process over, traceId = %s\n" , traceId)
} else {
fmt.Println("process over, no traceId" )
}
}
context可以信号通知取消goroutine
1. 设想一个场景,打开外卖订单,上面显示外卖小哥的位置,而且每秒更新一次,app向后端发起websocket链接后,
后台启动一个协程,每隔一秒计算一次小哥的位置并发送给前端,如果用户退出订单页面,后台需要取消此过程,
退出goroutine,系统回收资源
func Perform(ctx context .Context ) {
for {
calculatePos()
sendResult()
select {
case <-ctx.Done ():
return
case <-time .After (time .Second ):
}
}
}
ctx, cancel := context .WithTimeout (context .Background (), time .Hour )
go Perform(ctx)
cancel()
2 . 注意一个细节,WithTimeout函数返回的context 和cancel是分开的,context 本身并没有取消函数,
这样做的原因是取消函数只能由外层函数调用,防止子节点contxt调用取消函数,从而严格控制信息的流向
由父节点context 流向子节点context
防止goroutine泄漏
1. 举一个例子,如果不用context取消,goroutine就会泄漏的例子
func gen () <-chan int {
ch := make (chan int )
var n int
go func () {
for {
ch <- n
n++
time.Sleep(time.Second)
}
}()
return ch
}
func main () {
for v := range gen(){
fmt.Println(v)
if v == 5 {
break
}
}
time.Sleep(time.Second * 5 )
}
2. 这是一个可以无限生成整数的协程,但如果我们只要产生的前5 个数,那么就会发生goroutine泄漏
当n == 5 , 直接break 掉,那么gen函数的协程就会执行无限循环,发生了goroutine泄漏
3. 用context改进这个例子
func gen (ctx context.Context) <-chan int {
ch := make (chan int )
go func () {
var n int
for {
select {
case <-ctx.Done():
return
case ch <- n:
n++
time.Sleep(time.Millisecond * 200 )
}
}
}()
return ch
}
func main () {
cancelCtx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
for v := range gen(cancelCtx){
fmt.Println(v)
if v == 5 {
cancelFunc()
break
}
}
}
4. 增加一个context, 在break 前调用cancel函数,取消子goroutine, gen()函数在接收到取消信号后,
直接退出,系统回收资源
context.Value的查找过程是怎样的
type valueCtx struct {
Context
key, val interface {}
}
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
func (c *valueCtx) Value(key interface {}) interface {} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
1 . 由于它直接将Context作为匿名字段,因此尽管它只实现了两个方法String ()和Value (),其它方法继承自Context
但它仍然是一个context,这是go语言的一个特点
2 . 创建valueCtx函数:
func WithValue (parent Context, key, val interface {}) Context {
if parent == nil {
panic ("cannot create context from nil parent" )
}
if key == nil {
panic ("nil key" )
}
if !reflectlite.TypeOf(key).Comparable() {
panic ("key is not comparable" )
}
return &valueCtx{parent, key, val}
}
3. 对key的要求是可比较,因为之后要通过key取出context中的值,可比较是必须的,通过层层传递context,
最终形成一棵树
4. 和链表有点像,只是它的方向相反,Context指向它的父节点,而链表指向下一个节点,
通过WithValue函数可以创建层层的valueCtx,存储goroutine间可以共享的变量,
取值的过程,实际上是一个递归查找的过程
func (c *valueCtx) Value(key interface {}) interface {} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
5 . 它会顺着链路一直往上找,比较当前节点的key是否是要查找的key,如果是,则直接返回value,
否则一直顺着context 往前,最终找到根节点(一般是emtpyCtx),直接返回一个nil,
所以用Value方法的时候要判断,结果是否为nil
6 . 因为查找方向是往上走的,所以父节点没法获取子节点的值,子节点却可以获取父节点的值,
7 . WithValue: 创建context 节点的过程实际上就是创建链表节点的过程,两个节点的key值是可以相等的,
但他们是两个不同的context 节点,查找的时候会先从当前节点查找,如果找不到一直找到最后根节点,
整体而言,用WithValue构造的其实是一个低效率的链表
8 . 注意:如果能用函数参数传递的尽量不要使用context 传参,context 尽量传递一些共享的变量如session,cookie等
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)