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,即使代码还不需要它。 - 即使函数允许,也不要传递
nilContext,如果不确定使用哪个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 等待子函数执行