Go 中的 context 包在与 API 和慢处理交互时可以派上用场,特别是在生产级的 Web 服务中。在这些场景中,您可能想要通知所有的 goroutine 停止运行并返回。在 Go 语言中 context 包允许您传递一个 "context" 到您的程序。 Context 如超时或截止日期(deadline)或通道,来指示停止运行和返回。例如,如果您正在执行一个 web 请求或运行一个系统命令,定义一个超时对生产级系统通常是个好主意。因为,如果您依赖的API运行缓慢,你不希望在系统上备份(back up)请求,因为它可能最终会增加负载并降低所有请求的执行效率。导致级联效应。这是超时或截止日期 context 派上用场的地方。
context 包允许以下方式创建和获得 context:
这个函数返回一个空 context。这只能用于高等级(在 main 或顶级请求处理中)。这能用于派生我们稍后谈及的其他 context 。
ctx := context.Background()
这个函数也是创建一个空 context。也只能用于高等级或当您不确定使用什么 context,或函数以后会更新以便接收一个 context 。这意味您(或维护者)计划将来要添加 context 到函数。
ctx := context.TODO()
有趣的是,查看代码,它与 background 完全相同。不同的是,静态分析工具可以使用它来验证 context 是否正确传递,这是一个重要的细节,因为静态分析工具可以帮助在早期发现潜在的错误,并且可以连接到 CI/CD 管道。
来自 https://golang.org/src/context/context.go:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
此函数接收 context 并返回派生 context,其中值 val 与 key 关联,并通过 context 树与 context 一起传递。这意味着一旦获得带有值的 context,从中派生的任何 context 都会获得此值。不建议使用 context 值传递关键参数,而是函数应接收签名中的那些值,使其显式化。
ctx := context.WithValue(context.Background(), key, "test")
这是它开始变得有趣的地方。此函数创建从传入的父 context 派生的新 context。父 context 可以是后台 context 或传递给函数的 context。
返回派生 context 和取消函数。只有创建它的函数才能调用取消函数来取消此 context。如果您愿意,可以传递取消函数,但是,强烈建议不要这样做。这可能导致取消函数的调用者没有意识到取消 context 的下游影响。可能存在源自此的其他 context,这可能导致程序以意外的方式运行。简而言之,永远不要传递取消函数。
ctx, cancel := context.WithCancel(context.Background())
此函数返回其父项的派生 context,当截止日期超过或取消函数被调用时,该 context 将被取消。例如,您可以创建一个将在以后的某个时间自动取消的 context,并在子函数中传递它。当因为截止日期耗尽而取消该 context 时,获此 context 的所有函数都会收到通知去停止运行并返回。
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
此函数类似于 context.WithDeadline。不同之处在于它将持续时间作为参数输入而不是时间对象。此函数返回派生 context,如果调用取消函数或超出超时持续时间,则会取消该派生 context。
ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second)
现在我们知道了如何创建 context(Background 和 TODO)以及如何派生 context(WithValue,WithCancel,Deadline 和 Timeout),让我们讨论如何使用它们。
在下面的示例中,您可以看到接受 context 的函数启动一个 goroutine 并等待 该 goroutine 返回或该 context 取消。select 语句帮助我们选择先发生的任何情况并返回。
<-ctx.Done()
一旦 Done 通道被关闭,这个 <-ctx.Done():
被选择。一旦发生这种情况,此函数应该放弃运行并准备返回。这意味着您应该关闭所有打开的管道,释放资源并从函数返回。有些情况下,释放资源可以阻止返回,比如做一些挂起的清理等等。在处理 context 返回时,您应该注意任何这样的可能性。
实例1
package main
import (
"fmt"
"golang.org/x/net/context"
"time"
)
func main() {
//context传值操作:Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。
myContext := context.WithValue(context.Background(), "key", "001") //对context进行传值
fmt.Println("传递的值是:",myContext.Value("key")) //接受context传递的值
//context开关作用
ctx, cancel := context.WithCancel(context.Background())
i:=0
go func() {
for {
select {
case <-ctx.Done():
return
default:
time.Sleep(time.Second)
fmt.Println("=============",i,"=================")
i++
// todo 需要执行的操作
}
}
}()
//10s之后调用context,结束go程
for {
time.Sleep(time.Second*10)
cancel() //调用context取消(执行<-ctx.Done()被触发)
break
}
fmt.Println("====执行结束===")
}
上面代码,就是context的简单使用,context.WithCancel(context.Background())会返回一个context,以及cancel函数,context作用就是插入goroutine中的,如果ctx.Done()有信号,那么就执行return,以达到掐断goroutine的目的。那什么时候ctx.Done触发呢?那就要看cancel函数什么时候被调用了。其实cancel函数被调用,ctx.Done就被触发。上面例子,就是简单的让主goroutine“睡10秒”,就调用cancel(),那么就是过了10秒后,之前创建的goroutine里面的ctx.Done就有信号了,goroutine退出。可以看出cancel就是一个开关,可以关掉一个或多个goroutine。context是线程安全的。例如,exec.CommandContext 不会关闭读取管道,直到命令执行了进程创建的所有分支(Github 问题:https://github.com/golang/go/issues/23019 ),这意味着如果等待 cmd.Wait() 直到外部命令的所有分支都已完成,则 context 取消不会使该函数立即返回。如果您使用超时或截止日期,您可能会发现这不能按预期运行。如果遇到任何此类问题,可以使用 time.After 实现超时。
3.2搜索测试程序
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
// Run the HTTP request in a goroutine and pass the response to f.
tr := &http.Transport{}
client := &http.Client{Transport: tr}
c := make(chan error, 1)
go func() { c <- f(client.Do(req)) }()
select {
case <-ctx.Done():
tr.CancelRequest(req)
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}
httpDo关键的地方在于
select {
case <-ctx.Done():
tr.CancelRequest(req)
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
要么ctx被取消,要么request请求出错。
context包通过构建树型关系的Context,来达到上一层Goroutine能对传递给下一层Goroutine的控制。对于处理一个Request请求操作,需要采用context来层层控制Goroutine,以及传递一些变量来共享。
Context对象的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个Context变量(它为Context树结构的根);在请求处理结束后,撤销此ctx变量,释放资源。
每次创建一个Goroutine,要么将原有的Context传递给Goroutine,要么创建一个子Context并传递给Goroutine。
Context能灵活地存储不同类型、不同数目的值,并且使多个Goroutine安全地读写其中的值。
当通过父Context对象创建子Context对象时,可同时获得子Context的一个撤销函数,这样父Context对象的创建环境就获得了对子Context将要被传递到的Goroutine的撤销权。
Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:使用Context的程序包需要遵循如下的原则来满足接口的一致性以及便于静态分析。
Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx;不要把Context存在一个结构体当中,显式地传入函数。Context变量需要作为第一个参数使用,一般命名为ctx;
Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use;即使方法允许,也不要传入一个nil的Context,如果你不确定你要用什么Context的时候传一个context.TODO;
Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions;使用context的Value相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数;
The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines;同样的Context可以用来传递到不同的goroutine中,Context在多个goroutine中是安全的;
在子Context被传递到的goroutine中,应该对该子Context的Done信道(channel)进行监控,一旦该信道被关闭(即上层运行环境撤销了本goroutine的执行),应主动终止对当前请求信息的处理,释放资源并返回。