了解Golang中的Context包

图片


Go中的上下文包对于与API和慢速流程的交互很有用,尤其是在处理Web请求的生产级系统中。 有了它的帮助,可以告知goroutine需要完成其工作。


以下是一个小型指南,可帮助您在项目中使用此软件包以及一些最佳实践和陷阱。


(注意:在许多软件包中都使用了上下文,例如,在使用Docker时 )。


开始之前


要使用上下文,您必须了解什么是goroutine和通道。 我将尝试简要地考虑它们。 如果您已经熟悉它们,请直接转到“上下文”部分。


古鲁丁


官方文件说:“ Gorutin是一种轻量级的执行流。” Goroutine比线程轻,因此管理它们的资源消耗相对较少。


沙箱


package main import "fmt" // ,   Hello func printHello() { fmt.Println("Hello from printHello") } func main() { //   //       go func(){fmt.Println("Hello inline")}() //     go printHello() fmt.Println("Hello from main") } 

如果运行此程序,您将看到仅打印Hello from main 。 实际上,两个goroutine都已开始,但是main却更早完成了。 因此,Goroutines需要一种方法来通知main执行结束,以便她等待。 在这里渠道对我们有所帮助。


频道


通道是goroutine之间的一种通信方式。 当您要将结果,错误或其他信息从一个goroutine传输到另一个goroutine时,可以使用它们。 通道具有不同的类型,例如, int类型的通道接收整数,而error类型的通道接收错误,等等。


假设我们有一个int类型的ch通道。 如果要向通道发送内容,则语法将为ch <- 1 。 您可以从渠道中获取以下信息: var := <- ch ,即 从通道中获取值并将其保存在var变量中。


以下代码说明了如何使用通道来确认goroutine已完成工作并将其值返回给main


注意: 等待组也可以用于同步,但是在本文中,我选择了代码示例的通道,因为稍后将在上下文部分中使用它们。


沙箱


 package main import "fmt" //       int   func printHello(ch chan int) { fmt.Println("Hello from printHello") //     ch <- 2 } func main() { //  .       make //       : // ch := make(chan int, 2),       . ch := make(chan int) //  .  ,    . //       go func(){ fmt.Println("Hello inline") //     ch <- 1 }() //     go printHello(ch) fmt.Println("Hello from main") //      //     ,    i := <- ch fmt.Println("Received ",i) //      //    ,      <- ch } 

语境


go中的上下文包允许您以某种“上下文”将数据传递到程序。 上下文(如超时,截止日期或通道)表示关闭并调用返回。


例如,如果您发出Web请求或执行系统命令,则对生产级系统使用超时将是一个好主意。 因为如果您正在访问的API速度很慢,则您不太可能希望在系统上累积请求,因为这可能导致在处理自己的请求时增加负载并降低性能。 结果是级联效应。


在这里,超时或截止日期可能是正确的。


上下文创建


上下文包允许您通过以下方式创建和继承上下文:


context.Background()ctx上下文


此函数返回一个空上下文。 仅应在较高级别(主要或最高级别的请求处理程序)中使用。 它可以用于获取其他上下文,我们将在后面讨论。


 ctx, cancel := context.Background() 

注意事项 trans .:原始文章中有一个错误,使用context.Background的正确示例如下:


 ctx := context.Background() 

context.TODO()ctx上下文


此函数还会创建一个空上下文。 并且,当您不确定要使用哪个上下文时,或者如果函数尚未收到所需的上下文时,也应仅在较高级别上使用它。 这意味着您(或支持代码的人)计划在以后向该功能添加上下文。


 ctx, cancel := context.TODO() 

注意事项 trans .:原始文章中有一个错误,使用context.TODO的正确示例如下:


 ctx := context.TODO() 

有趣的是,看一下代码 ,它与背景绝对相同。 唯一的区别是,在这种情况下,您可以使用静态分析工具来检查上下文传输的有效性,这是一个重要的细节,因为这些工具有助于在早期阶段识别潜在的错误,并且可以包含在CI / CD管道中。


从这里


 var ( background = new(emptyCtx) todo = new(emptyCtx) ) 

context.WithValue(父上下文,键,val接口{})(ctx上下文,取消CancelFunc)


注意事项 莱恩:原始文章中有一个错误, context.WithValue的正确签名context.WithValue将如下所示:


 context.WithValue(parent Context, key, val interface{}) Context 

该函数获取一个上下文,并返回从中派生的上下文,其中val值与key关联,并通过整个上下文树。 也就是说,一旦创建WithValue上下文,任何派生的上下文都会收到该值。


不建议使用上下文值传递关键参数;相反,函数应在签名中显式地使用它们。


 ctx := context.WithValue(context.Background(), key, "test") 

context.WithCancel(父上下文)(ctx上下文,取消CancelFunc)


这里变得更加有趣。 此函数从传递给它的父级创建一个新的上下文。 父级可以是背景上下文,也可以是作为参数传递给函数的上下文。


返回派生的上下文和撤消函数。 只有创建它的函数才应调用该函数以取消上下文。 您可以根据需要将undo函数传递给其他函数,但是强烈建议不要这样做。 通常,此决定是基于对上下文取消的误解而做出的。 因此,从该父对象生成的上下文可能会影响程序,从而导致意外结果。 简而言之,最好不要通过取消功能。


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

注意事项 Lane:在原始文章中,作者显然是针对context.WithCancel给出了一个context.WithCancel的示例。 context.WithCancel的正确示例为:


 ctx, cancel := context.WithCancel(context.Background()) 

context.WithDeadline(父上下文,d time.Time)(ctx上下文,取消CancelFunc)


此函数从其父级返回派生上下文,该上下文在截止日期或调用cancel函数后被取消。 例如,您可以创建一个在特定时间自动取消的上下文,并将其传递给子函数。 在截止日期之后取消此上下文时,应通过通知来通知具有此上下文的所有功能。


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

context.WithTimeout(父上下文,超时时间。持续时间)(ctx上下文,取消CancelFunc)


此功能类似于context.WithDeadline。 区别在于将时间长度用作输入。 此函数返回派生的上下文,该上下文在调用cancel函数或一段时间后被取消。


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

注意事项 Lane:在原始文章中,作者显然是针对context.WithTimeout给出了一个context.WithTimeout的示例。 context.WithTimeout的正确示例是这样的:


 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) 

在函数中接收和使用上下文


现在,我们知道如何创建上下文(背景和TODO)以及如何生成上下文(WithValue,WithCancel,Deadline和Timeout),让我们讨论如何使用它们。


在下面的示例中,您可以看到获取上下文的函数启动了goroutine并期望它返回或取消上下文。 select语句帮助我们确定首先发生什么并终止函数。


关闭通道Done <-ctx.Done() ,选择案例case <-ctx.Done(): 一旦发生这种情况,该函数应中断工作并准备返回。 这意味着您必须关闭所有打开的连接,释放资源并从函数中返回。 有时资源的释放可能会延迟返回,例如,清理挂起。 您必须牢记这一点。


本节后面的示例是一个完全完成的go程序,它说明了超时和撤消功能。


 // ,  -      // ,   -    func sleepRandomContext(ctx context.Context, ch chan bool) { //  (. .:  )    //     // ,    defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //   sleeptimeChan := make(chan int) //       //     go sleepRandom("sleepRandomContext", sleeptimeChan) //  select        select { case <-ctx.Done(): //   ,    //  ,     -   //    ,    ( ) //    -  , //    ,   //         fmt.Println("Time to return") case sleeptime := <-sleeptimeChan: //   ,       fmt.Println("Slept for ", sleeptime, "ms") } } 

例子


正如我们所看到的,使用上下文可以处理截止日期,超时,还可以调用cancel函数,从而使用派生上下文使所有函数都清楚需要完成工作并执行返回。 考虑一个例子:


main功能:


  • 创建一个取消函数上下文
  • 任意超时后调用取消功能

doWorkContext函数:


  • 创建带有超时的派生上下文
  • 当主函数调用cancelFunction,超时到期或doWorkContext调用其cancelFunction时,将取消此上下文。
  • 运行goroutine来执行一些缓慢的任务,并传递结果上下文
  • 等待goroutine完成或从main取消上下文,以先到者为准

sleepRandomContext函数:


  • 启动goroutine以执行一些缓慢的任务
  • 等待goroutine完成,或者
  • 等待上下文被主函数取消,超时或调用其自己的cancelFunction

sleepRandom函数:


  • 随机入睡

本示例使用睡眠模式来模拟随机处理时间,但实际上,您可以使用通道向此功能发出信号以告知清洁开始,并等待通道确认清洁已完成。


沙箱 (看来我在沙箱中使用的随机时间几乎没有变化。请在您的本地计算机上尝试一下以查看随机性)


Github


 package main import ( "context" "fmt" "math/rand" "Time" ) //   func sleepRandom(fromFunction string, ch chan int) { //    defer func() { fmt.Println(fromFunction, "sleepRandom complete") }() //    //   , // «»      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") //   ,     if ch != nil { ch <- sleeptime } } // ,       // ,   -    func sleepRandomContext(ctx context.Context, ch chan bool) { //  (. .:  )    //     // ,    defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //   sleeptimeChan := make(chan int) //       //     go sleepRandom("sleepRandomContext", sleeptimeChan) //  select        select { case <-ctx.Done(): //   ,    //  ,    doWorkContext  // doWorkContext  main  cancelFunction //  ,     -   //    ,    ( ) //    -  , //    ,   //         fmt.Println("sleepRandomContext: Time to return") case sleeptime := <-sleeptimeChan: //   ,       fmt.Println("Slept for ", sleeptime, "ms") } } //  ,         //       //   ,      main func doWorkContext(ctx context.Context) { //          - //  150  //  ,   ,   150  ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond) //         defer func() { fmt.Println("doWorkContext complete") cancelFunction() }() //       //         , //      ,    ch := make(chan bool) go sleepRandomContext(ctxWithTimeout, ch) //  select      select { case <-ctx.Done(): //   ,           //     ,   main   cancelFunction fmt.Println("doWorkContext: Time to return") case <-ch: //   ,       fmt.Println("sleepRandomContext returned") } } func main() { //   background ctx := context.Background() //     ctxWithCancel, cancelFunction := context.WithCancel(ctx) //      //        defer func() { fmt.Println("Main Defer: canceling context") cancelFunction() }() //     - //   ,        go func() { sleepRandom("Main", nil) cancelFunction() fmt.Println("Main Sleep complete. canceling context") }() //   doWorkContext(ctxWithCancel) } 

陷阱


如果函数使用上下文,请确保正确处理了取消通知。 例如, exec.CommandContext在命令完成该进程( Github )创建的所有派生之前不会关闭读取通道,即,如果您等待cmd.Wait(),则取消上下文不会立即从函数返回。直到外部命令的所有分支完成处理。


如果您使用的超时或截止日期具有最大的运行时间,则可能无法按预期运行。 在这种情况下,最好使用time.After实现超时。


最佳实务


  1. context.Background只能在最高级别使用,作为所有派生上下文的根。
  2. 当您不确定要使用什么时,或者当前函数将来将使用上下文时,应使用context.TODO。
  3. 建议取消上下文,但是清除和退出这些功能可能需要一些时间。
  4. context.Value应该尽可能少地使用,并且不应用于传递可选参数。 这会使API难以理解,并可能导致错误。 这些值应作为参数传递。
  5. 不要将上下文存储在结构中;应在函数中显式传递它们,最好将其作为第一个参数。
  6. 切勿将nil上下文作为参数传递。 如有疑问,请使用TODO。
  7. Context结构没有取消方法,因为只有产生上下文的函数才应取消它。

来自翻译


在我们公司中,当开发供内部使用的服务器应用程序时,我们会积极使用Context包。 但是,除了上下文之外,此类用于正常运行的应用程序还需要其他元素,例如:


  • 记录中
  • 用于应用程序终止,重新加载和对数旋转的信号处理
  • 使用pid文件
  • 使用配置文件

因此,在某个时候,我们决定总结所有积累的经验,并创建辅助软件包,这些软件包大大简化了应用程序(尤其是具有API的应用程序)的编写。 我们已将我们的开发发布到公共领域,任何人都可以使用它们。 以下是一些可用于解决此类问题的软件包的链接:



另请阅读我们博客上的其他文章:


Source: https://habr.com/ru/post/zh-CN461723/


All Articles