context是Go语言官方定义的一个包,称之为上下文。html
Go中的context包在与API和慢进程交互时能够派上用场,特别是在提供Web请求的生产级系统中。在哪里,您可能想要通知全部goroutines中止工做并返回。git
这是一个基本教程,介绍如何在项目中使用它以及一些最佳实践和陷阱。程序员
在了解上下文以前,请先了解如下概念github
在Go语言中 context 包容许您传递一个 "context" 到您的程序,如超时或截止日期(deadline)或通道(channel),以及指示中止运行和返回等。例如,若是您正在执行Web请求或运行系统命令,那么对生产级系统进行超时控制一般是个好主意。由于,若是您依赖的API运行缓慢,您不但愿在系统上备份请求,这可能最终会增长负载并下降您所服务的全部请求的性能。致使级联效应。这是超时或截止日期context能够派上用场的地方。golang
这里咱们先来分析context源码( https://golang.org/src/contex...)。安全
context
包的核心就是Context
接口,其定义以下:服务器
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
这个接口共有4个方法:app
Done
方法返回一个只读的chan,类型为struct{}
,咱们在goroutine中,若是该方法返回的chan能够读取,则意味着parent context已经发起了取消请求,咱们经过Done
方法收到这个信号后,就应该作清理操做,而后退出goroutine,释放资源。Err
方法返回取消的错误缘由,由于什么Context被取消。Value
方法获取该Context上绑定的值,是一个键值对,因此要经过一个Key才能够获取对应的值,这个值通常是线程安全的。但使用这些数据的时候要注意同步,好比返回了一个map,而这个map的读写则要加锁。以上四个方法中经常使用的就是Done
了,若是Context取消的时候,咱们就能够获得一个关闭的chan,关闭的chan是能够读取的,因此只要能够读取的时候,就意味着收到Context取消的信号了,如下是这个方法的经典用法。dom
func Stream(ctx context.Context, out chan<- Value) error { for { v, err := DoSomething(ctx) if err != nil { return err } select { case <-ctx.Done(): return ctx.Err() case out <- v: } } }
Context接口并不须要咱们实现,Go内置已经帮咱们实现了2个(Background、TODO),咱们代码中最开始都是以这两个内置的做为最顶层的partent context(即根context),衍生出更多的子Context。函数
var ( background = new(emptyCtx) todo = new(emptyCtx) ) func Background() Context { return background } func TODO() Context { return todo }
Background:主要用于main函数、初始化以及测试代码中,做为Context这个树结构的最顶层的Context,也就是根Context。
TODO:在还不肯定使用context的场景,可能当前函数之后会更新以便使用 context。
这两个函数的本质是emptyCtx结构体类型。
type emptyCtx int func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { return } func (*emptyCtx) Done() <-chan struct{} { return nil } func (*emptyCtx) Err() error { return nil } func (*emptyCtx) Value(key interface{}) interface{} { return nil }
这就是emptyCtx
实现Context接口的方法,能够看到,这些方法什么都没作,返回的都是nil或者零值。
有上面的根context,那么是如何衍生更多的子Context的呢?这就要靠context包为咱们提供的With
系列的函数了。
一、取消函数
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
此函数接收一个parent Context参数,父 context 能够是后台 context 或传递给函数的 context。
返回派生 context 和取消函数。只有建立它的函数才能调用取消函数来取消此 context。若是您愿意,能够传递取消函数,可是,强烈建议不要这样作。这可能致使取消函数的调用者没有意识到取消 context 的下游影响。可能存在源自此的其余 context,这可能致使程序以意外的方式运行。简而言之,永远不要传递取消函数。
示例
package main import ( "fmt" "time" "golang.org/x/net/context" ) func main() { //建立一个可取消子context,context.Background():返回一个空的Context,这个空的Context通常用于整个Context树的根节点。 ctx, cancel := context.WithCancel(context.Background()) go func(ctx context.Context) { for { select { //使用select调用<-ctx.Done()判断是否要结束 case <-ctx.Done(): fmt.Println("goroutine exit") return default: fmt.Println("goroutine running.") time.Sleep(2 * time.Second) } } }(ctx) time.Sleep(10 * time.Second) fmt.Println("main fun exit") //取消context cancel() time.Sleep(5 * time.Second) }
二、超时控制
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc):
此函数返回其父项的派生 context,当截止日期超过或取消函数被调用时,该 context 将被取消。例如,您能够建立一个将在之后的某个时间自动取消的 context,并在子函数中传递它。当由于截止日期耗尽而取消该 context 时,获此 context 的全部函数都会收到通知去中止运行并返回。
示例
package main import ( "fmt" "golang.org/x/net/context" "time" ) func main() { d := time.Now().Add(2 * time.Second) //设置超时控制WithDeadline,超时时间2 ctx, cancel := context.WithDeadline(context.Background(), d) defer cancel() select { case <-time.After(3 * time.Second): fmt.Println("timeout") case <-ctx.Done(): //2到了到了,执行该代码 fmt.Println(ctx.Err()) } }
三、超时控制
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc):
此函数相似于 context.WithDeadline。不一样之处在于它将持续时间做为参数输入而不是时间对象。此函数返回派生 context,若是调用取消函数或超出超时持续时间,则会取消该派生 context。
package main import ( "fmt" "golang.org/x/net/context" "time" ) func main() { //设置超时控制WithDeadline,超时时间2 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() select { case <-time.After(3 * time.Second): fmt.Println("timeout") case <-ctx.Done(): //2到了到了,执行该代码 fmt.Println(ctx.Err()) } }
四、返回派生的context
func WithValue(parent Context, key, val interface{}) Context:
此函数接收 context 并返回派生 context,其中值 val 与 key 关联,并经过 context 树与 context 一块儿传递。这意味着一旦得到带有值的 context,从中派生的任何 context 都会得到此值。不建议使用 context 值传递关键参数,而是函数应接收签名中的那些值,使其显式化。
示例
package main import ( "context" "fmt" ) func Route(ctx context.Context) { ret, ok := ctx.Value("id").(int) if !ok { ret = 1 } fmt.Printf("id:%d\n", ret) s, _ := ctx.Value("name").(string) fmt.Printf("name:%s\n", s) } func main() { ctx := context.WithValue(context.Background(), "id", 123) ctx = context.WithValue(ctx, "name", "jerry") Route(ctx) }
在下面的示例中,您能够看到接受context的函数启动goroutine并等待返回该goroutine或取消该context。select语句帮助咱们选择先发生的任何状况并返回。
<-ctx.Done()
关闭“完成”通道后,将case <-ctx.Done():
选中该通道。一旦发生这种状况,该功能应该放弃工做并准备返回。这意味着您应该关闭全部打开的管道,释放资源并从函数返回。有些状况下,释放资源能够阻止返回,好比作一些挂起的清理等等。在处理context返回时,你应该注意任何这样的可能性。
本节后面的示例有一个完整的go程序,它说明了超时和取消功能。
//Function that does slow processing with a context //Note that context is the first argument func sleepRandomContext(ctx context.Context, ch chan bool) { //Cleanup tasks //There are no contexts being created here //Hence, no canceling needed defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //Make a channel sleeptimeChan := make(chan int) //Start slow processing in a goroutine //Send a channel for communication go sleepRandom("sleepRandomContext", sleeptimeChan) //Use a select statement to exit out if context expires select { case <-ctx.Done(): //If context expires, this case is selected //Free up resources that may no longer be needed because of aborting the work //Signal all the goroutines that should stop work (use channels) //Usually, you would send something on channel, //wait for goroutines to exit and then return //Or, use wait groups instead of channels for synchronization fmt.Println("Time to return") case sleeptime := <-sleeptimeChan: //This case is selected when processing finishes before the context is cancelled fmt.Println("Slept for ", sleeptime, "ms") } }
到目前为止,咱们已经看到使用 context 能够设置截止日期,超时或调用取消函数来通知全部使用任何派生 context 的函数来中止运行并返回。如下是它如何工做的示例:
main 函数
doWorkContext 函数
这个 context 将被取消当
sleepRandomContext 函数
sleepRandom 函数
Playground: https://play.golang.org/p/grQ... (看起来我使用的随机种子,在 playground 时间没有真正改变,您须要在你本机执行去看随机性)
Github: https://github.com/pagnihotry...
package main import ( "context" "fmt" "math/rand" "time" ) //Slow function func sleepRandom(fromFunction string, ch chan int) { //defer cleanup defer func() { fmt.Println(fromFunction, "sleepRandom complete") }() //Perform a slow task //For illustration purpose, //Sleep here for random ms seed := time.Now().UnixNano() r := rand.New(rand.NewSource(seed)) randomNumber := r.Intn(100) sleeptime := randomNumber + 100 fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms") time.Sleep(time.Duration(sleeptime) * time.Millisecond) fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms") //write on the channel if it was passed in if ch != nil { ch <- sleeptime } } //Function that does slow processing with a context //Note that context is the first argument func sleepRandomContext(ctx context.Context, ch chan bool) { //Cleanup tasks //There are no contexts being created here //Hence, no canceling needed defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //Make a channel sleeptimeChan := make(chan int) //Start slow processing in a goroutine //Send a channel for communication go sleepRandom("sleepRandomContext", sleeptimeChan) //Use a select statement to exit out if context expires select { case <-ctx.Done(): //If context is cancelled, this case is selected //This can happen if the timeout doWorkContext expires or //doWorkContext calls cancelFunction or main calls cancelFunction //Free up resources that may no longer be needed because of aborting the work //Signal all the goroutines that should stop work (use channels) //Usually, you would send something on channel, //wait for goroutines to exit and then return //Or, use wait groups instead of channels for synchronization fmt.Println("sleepRandomContext: Time to return") case sleeptime := <-sleeptimeChan: //This case is selected when processing finishes before the context is cancelled fmt.Println("Slept for ", sleeptime, "ms") } } //A helper function, this can, in the real world do various things. //In this example, it is just calling one function. //Here, this could have just lived in main func doWorkContext(ctx context.Context) { //Derive a timeout context from context with cancel //Timeout in 150 ms //All the contexts derived from this will returns in 150 ms ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond) //Cancel to release resources once the function is complete defer func() { fmt.Println("doWorkContext complete") cancelFunction() }() //Make channel and call context function //Can use wait groups as well for this particular case //As we do not use the return value sent on channel ch := make(chan bool) go sleepRandomContext(ctxWithTimeout, ch) //Use a select statement to exit out if context expires select { case <-ctx.Done(): //This case is selected when the passed in context notifies to stop work //In this example, it will be notified when main calls cancelFunction fmt.Println("doWorkContext: Time to return") case <-ch: //This case is selected when processing finishes before the context is cancelled fmt.Println("sleepRandomContext returned") } } func main() { //Make a background context ctx := context.Background() //Derive a context with cancel ctxWithCancel, cancelFunction := context.WithCancel(ctx) //defer canceling so that all the resources are freed up //For this and the derived contexts defer func() { fmt.Println("Main Defer: canceling context") cancelFunction() }() //Cancel context after a random time //This cancels the request after a random timeout //If this happens, all the contexts derived from this should return go func() { sleepRandom("Main", nil) cancelFunction() fmt.Println("Main Sleep complete. canceling context") }() //Do work doWorkContext(ctxWithCancel) }
cmd.Wait()
外部命令的全部分支都已完成处理。若是您使用超时或最后执行时间的最后期限,您可能会发现这不能按预期工做。若是遇到任何此类问题,可使用执行超时time.After
。这就意味着若是您正在编写一个具备可能须要大量时间的函数的库,而且您的库可能会被服务器应用程序使用,那么您必须接受这些函数中的context。固然,我能够context.TODO()
随处经过,但这形成程序可读性差,程序看起来不够优雅。
Context
struct没有cancel方法,由于只有派生context的函数才能取消它。http://p.agnihotry.com/post/u...
https://www.flysnow.org/2017/...
https://faiface.github.io/pos...