
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
实现超时。
最佳实务
- context.Background只能在最高级别使用,作为所有派生上下文的根。
- 当您不确定要使用什么时,或者当前函数将来将使用上下文时,应使用context.TODO。
- 建议取消上下文,但是清除和退出这些功能可能需要一些时间。
- context.Value应该尽可能少地使用,并且不应用于传递可选参数。 这会使API难以理解,并可能导致错误。 这些值应作为参数传递。
- 不要将上下文存储在结构中;应在函数中显式传递它们,最好将其作为第一个参数。
- 切勿将nil上下文作为参数传递。 如有疑问,请使用TODO。
Context
结构没有取消方法,因为只有产生上下文的函数才应取消它。
来自翻译
在我们公司中,当开发供内部使用的服务器应用程序时,我们会积极使用Context包。 但是,除了上下文之外,此类用于正常运行的应用程序还需要其他元素,例如:
- 记录中
- 用于应用程序终止,重新加载和对数旋转的信号处理
- 使用pid文件
- 使用配置文件
- 等
因此,在某个时候,我们决定总结所有积累的经验,并创建辅助软件包,这些软件包大大简化了应用程序(尤其是具有API的应用程序)的编写。 我们已将我们的开发发布到公共领域,任何人都可以使用它们。 以下是一些可用于解决此类问题的软件包的链接:
另请阅读我们博客上的其他文章: