Go 中的 context 包在与 API 和慢处理交互时可以派上用场,特别是在生产级的 Web 服务中。在这些场景中,您可能想要通知所有的 goroutine 停止运行并返回。这是一个基本教程,介绍如何在项目中使用它以及一些最佳实践和陷阱。
要理解 context 包,您应该熟悉两个概念。
在转到 context 之前,我将简要介绍这些内容,如果您已经熟悉,则可以直接转到 context 部分。

Goroutine

来自 Go 语言官方文档:”goroutine 是一个轻量级的执行线程”。多个 goroutine 比一个线程轻量所以管理它们消耗的资源相对更少。
Playground: https://play.golang.org/p/-TDMgnkJRY6

  1. package main
  2. import "fmt"
  3. //function to print hello
  4. func printHello() {
  5. fmt.Println("Hello from printHello")
  6. }
  7. func main() {
  8. //inline goroutine. Define a function inline and then call it.
  9. go func(){fmt.Println("Hello inline")}()
  10. //call a function as goroutine
  11. go printHello()
  12. fmt.Println("Hello from main")
  13. }

如果您运行上面的程序,您只能看到 main 中打印的 Hello, 因为它启动了两个 goroutine 并在它们完成前退出了。为了让 main 等待这些 goroutine 执行完,您需要一些方法让这些 goroutine 告诉 main 它们执行完了,那就需要用到通道。

通道(channel)

这是 goroutine 之间的沟通渠道。当您想要将结果或错误,或任何其他类型的信息从一个 goroutine 传递到另一个 goroutine 时就可以使用通道。通道是有类型的,可以是 int 类型的通道接收整数或错误类型的接收错误等。
假设有个 int 类型的通道 ch,如果你想发一些信息到这个通道,语法是 ch <- 1,如果你想从这个通道接收一些信息,语法就是 var := <-ch。这将从这个通道接收并存储值到 var 变量。
以下程序说明了通道的使用确保了 goroutine 执行完成并将值返回给 main 。
注意:WaitGroup( https://golang.org/pkg/sync/#WaitGroup )也可用于同步,但稍后在 context 部分我们谈及通道,所以在这篇博客中的示例代码,我选择了它们。
Playground: https://play.golang.org/p/3zfQMox5mHn

  1. package main
  2. import "fmt"
  3. //prints to stdout and puts an int on channel
  4. func printHello(ch chan int) {
  5. fmt.Println("Hello from printHello")
  6. //send a value on channel
  7. ch <- 2
  8. }
  9. func main() {
  10. //make a channel. You need to use the make function to create channels.
  11. //channels can also be buffered where you can specify size. eg: ch := make(chan int, 2)
  12. //that is out of the scope of this post.
  13. ch := make(chan int)
  14. //inline goroutine. Define a function and then call it.
  15. //write on a channel when done
  16. go func() {
  17. fmt.Println("Hello inline")
  18. //send a value on channel
  19. ch <- 1
  20. }()
  21. //call a function as goroutine
  22. go printHello(ch)
  23. fmt.Println("Hello from main")
  24. //get first value from channel.
  25. //and assign to a variable to use this value later
  26. //here that is to print it
  27. i := <-ch
  28. fmt.Println("Recieved ", i)
  29. //get the second value from channel
  30. //do not assign it to a variable because we dont want to use that
  31. <-ch
  32. }

在 Go 语言中 context 包允许您传递一个 “context” 到您的程序。 Context 如超时或截止日期(deadline)或通道,来指示停止运行和返回。例如,如果您正在执行一个 web 请求或运行一个系统命令,定义一个超时对生产级系统通常是个好主意。因为,如果您依赖的API运行缓慢,你不希望在系统上备份(back up)请求,因为它可能最终会增加负载并降低所有请求的执行效率。导致级联效应。这是超时或截止日期 context 派上用场的地方。

创建 context

context 包允许以下方式创建和获得 context:

context.Background() Context

这个函数返回一个空 context。这只能用于高等级(在 main 或顶级请求处理中)。这能用于派生我们稍后谈及的其他 context 。

  1. ctx := context.Background()

context.TODO() Context

这个函数也是创建一个空 context。也只能用于高等级或当您不确定使用什么 context,或函数以后会更新以便接收一个 context 。这意味您(或维护者)计划将来要添加 context 到函数。

  1. ctx := context.TODO()

有趣的是,查看代码,它与 background 完全相同。不同的是,静态分析工具可以使用它来验证 context 是否正确传递,这是一个重要的细节,因为静态分析工具可以帮助在早期发现潜在的错误,并且可以连接到 CI/CD 管道。
来自 https://golang.org/src/context/context.go:

  1. var (
  2. background = new(emptyCtx)
  3. todo = new(emptyCtx)
  4. )

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

此函数接收 context 并返回派生 context,其中值 val 与 key 关联,并通过 context 树与 context 一起传递。这意味着一旦获得带有值的 context,从中派生的任何 context 都会获得此值。不建议使用 context 值传递关键参数,而是函数应接收签名中的那些值,使其显式化。

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

context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)

这是它开始变得有趣的地方。此函数创建从传入的父 context 派生的新 context。父 context 可以是后台 context 或传递给函数的 context。
返回派生 context 和取消函数。只有创建它的函数才能调用取消函数来取消此 context。如果您愿意,可以传递取消函数,但是,强烈建议不要这样做。这可能导致取消函数的调用者没有意识到取消 context 的下游影响。可能存在源自此的其他 context,这可能导致程序以意外的方式运行。简而言之,永远不要传递取消函数。

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

context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)

此函数返回其父项的派生 context,当截止日期超过或取消函数被调用时,该 context 将被取消。例如,您可以创建一个将在以后的某个时间自动取消的 context,并在子函数中传递它。当因为截止日期耗尽而取消该 context 时,获此 context 的所有函数都会收到通知去停止运行并返回。

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

context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)

此函数类似于 context.WithDeadline。不同之处在于它将持续时间作为参数输入而不是时间对象。此函数返回派生 context,如果调用取消函数或超出超时持续时间,则会取消该派生 context。

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

函数接收和使用 Context

现在我们知道了如何创建 context(Background 和 TODO)以及如何派生 context(WithValue,WithCancel,Deadline 和 Timeout),让我们讨论如何使用它们。
在下面的示例中,您可以看到接受 context 的函数启动一个 goroutine 并等待 该 goroutine 返回或该 context 取消。select 语句帮助我们选择先发生的任何情况并返回。
<-ctx.Done() 一旦 Done 通道被关闭,这个 <-ctx.Done(): 被选择。一旦发生这种情况,此函数应该放弃运行并准备返回。这意味着您应该关闭所有打开的管道,释放资源并从函数返回。有些情况下,释放资源可以阻止返回,比如做一些挂起的清理等等。在处理 context 返回时,您应该注意任何这样的可能性。
本节后面的示例有一个完整的 Go 语言程序,它说明了超时和取消功能。

  1. //Function that does slow processing with a context
  2. //Note that context is the first argument
  3. func sleepRandomContext(ctx context.Context, ch chan bool) {
  4. //Cleanup tasks
  5. //There are no contexts being created here
  6. //Hence, no canceling needed
  7. defer func() {
  8. fmt.Println("sleepRandomContext complete")
  9. ch <- true
  10. }()
  11. //Make a channel
  12. sleeptimeChan := make(chan int)
  13. //Start slow processing in a goroutine
  14. //Send a channel for communication
  15. go sleepRandom("sleepRandomContext", sleeptimeChan)
  16. //Use a select statement to exit out if context expires
  17. select {
  18. case <-ctx.Done():
  19. //If context expires, this case is selected
  20. //Free up resources that may no longer be needed because of aborting the work
  21. //Signal all the goroutines that should stop work (use channels)
  22. //Usually, you would send something on channel,
  23. //wait for goroutines to exit and then return
  24. //Or, use wait groups instead of channels for synchronization
  25. fmt.Println("Time to return")
  26. case sleeptime := <-sleeptimeChan:
  27. //This case is selected when processing finishes before the context is cancelled
  28. fmt.Println("Slept for ", sleeptime, "ms")
  29. }
  30. }

例子

到目前为止,我们已经看到使用 context 可以设置截止日期,超时或调用取消函数来通知所有使用任何派生 context 的函数来停止运行并返回。以下是它如何工作的示例:
main 函数

  • 用 cancel 创建一个 context
  • 随机超时后调用取消函数

doWorkContext 函数

  • 派生一个超时 context
  • 这个 context 将被取消当
    • main 调用取消函数或
    • 超时到或
    • doWorkContext 调用它的取消函数
  • 启动 goroutine 传入派生上下文执行一些慢处理
  • 等待 goroutine 完成或上下文被 main goroutine 取消,以优先发生者为准

sleepRandomContext 函数

  • 开启一个 goroutine 去做些缓慢的处理
  • 等待该 goroutine 完成或,
  • 等待 context 被 main goroutine 取消,操时或它自己的取消函数被调用

sleepRandom 函数

  • 随机时间休眠
  • 此示例使用休眠来模拟随机处理时间,在实际示例中,您可以使用通道来通知此函数,以开始清理并在通道上等待它,以确认清理已完成。

Playground: https://play.golang.org/p/grQAUN3MBlg (看起来我使用的随机种子,在 playground 时间没有真正改变,您需要在你本机执行去看随机性)
Github: https://github.com/pagnihotry/golang_samples/blob/master/go_context_sample.go

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. "math/rand"
  6. "time"
  7. )
  8. //Slow function
  9. func sleepRandom(fromFunction string, ch chan int) {
  10. //defer cleanup
  11. defer func() { fmt.Println(fromFunction, "sleepRandom complete") }()
  12. //Perform a slow task
  13. //For illustration purpose,
  14. //Sleep here for random ms
  15. seed := time.Now().UnixNano()
  16. r := rand.New(rand.NewSource(seed))
  17. randomNumber := r.Intn(100)
  18. sleeptime := randomNumber + 100
  19. fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms")
  20. time.Sleep(time.Duration(sleeptime) * time.Millisecond)
  21. fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms")
  22. //write on the channel if it was passed in
  23. if ch != nil {
  24. ch <- sleeptime
  25. }
  26. }
  27. //Function that does slow processing with a context
  28. //Note that context is the first argument
  29. func sleepRandomContext(ctx context.Context, ch chan bool) {
  30. //Cleanup tasks
  31. //There are no contexts being created here
  32. //Hence, no canceling needed
  33. defer func() {
  34. fmt.Println("sleepRandomContext complete")
  35. ch <- true
  36. }()
  37. //Make a channel
  38. sleeptimeChan := make(chan int)
  39. //Start slow processing in a goroutine
  40. //Send a channel for communication
  41. go sleepRandom("sleepRandomContext", sleeptimeChan)
  42. //Use a select statement to exit out if context expires
  43. select {
  44. case <-ctx.Done():
  45. //If context is cancelled, this case is selected
  46. //This can happen if the timeout doWorkContext expires or
  47. //doWorkContext calls cancelFunction or main calls cancelFunction
  48. //Free up resources that may no longer be needed because of aborting the work
  49. //Signal all the goroutines that should stop work (use channels)
  50. //Usually, you would send something on channel,
  51. //wait for goroutines to exit and then return
  52. //Or, use wait groups instead of channels for synchronization
  53. fmt.Println("sleepRandomContext: Time to return")
  54. case sleeptime := <-sleeptimeChan:
  55. //This case is selected when processing finishes before the context is cancelled
  56. fmt.Println("Slept for ", sleeptime, "ms")
  57. }
  58. }
  59. //A helper function, this can, in the real world do various things.
  60. //In this example, it is just calling one function.
  61. //Here, this could have just lived in main
  62. func doWorkContext(ctx context.Context) {
  63. //Derive a timeout context from context with cancel
  64. //Timeout in 150 ms
  65. //All the contexts derived from this will returns in 150 ms
  66. ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond)
  67. //Cancel to release resources once the function is complete
  68. defer func() {
  69. fmt.Println("doWorkContext complete")
  70. cancelFunction()
  71. }()
  72. //Make channel and call context function
  73. //Can use wait groups as well for this particular case
  74. //As we do not use the return value sent on channel
  75. ch := make(chan bool)
  76. go sleepRandomContext(ctxWithTimeout, ch)
  77. //Use a select statement to exit out if context expires
  78. select {
  79. case <-ctx.Done():
  80. //This case is selected when the passed in context notifies to stop work
  81. //In this example, it will be notified when main calls cancelFunction
  82. fmt.Println("doWorkContext: Time to return")
  83. case <-ch:
  84. //This case is selected when processing finishes before the context is cancelled
  85. fmt.Println("sleepRandomContext returned")
  86. }
  87. }
  88. func main() {
  89. //Make a background context
  90. ctx := context.Background()
  91. //Derive a context with cancel
  92. ctxWithCancel, cancelFunction := context.WithCancel(ctx)
  93. //defer canceling so that all the resources are freed up
  94. //For this and the derived contexts
  95. defer func() {
  96. fmt.Println("Main Defer: canceling context")
  97. cancelFunction()
  98. }()
  99. //Cancel context after a random time
  100. //This cancels the request after a random timeout
  101. //If this happens, all the contexts derived from this should return
  102. go func() {
  103. sleepRandom("Main", nil)
  104. cancelFunction()
  105. fmt.Println("Main Sleep complete. canceling context")
  106. }()
  107. //Do work
  108. doWorkContext(ctxWithCancel)
  109. }

缺陷

如果函数接收 context 参数,确保检查它是如何处理取消通知的。例如,exec.CommandContext 不会关闭读取管道,直到命令执行了进程创建的所有分支(Github 问题:https://github.com/golang/go/issues/23019 ),这意味着如果等待 cmd.Wait() 直到外部命令的所有分支都已完成,则 context 取消不会使该函数立即返回。如果您使用超时或截止日期,您可能会发现这不能按预期运行。如果遇到任何此类问题,可以使用 time.After 实现超时。

最佳实践

  1. context.Background 只应用在最高等级,作为所有派生 context 的根。
  2. context.TODO 应用在不确定要使用什么的地方,或者当前函数以后会更新以便使用 context。
  3. context 取消是建议性的,这些函数可能需要一些时间来清理和退出。
  4. context.Value 应该很少使用,它不应该被用来传递可选参数。这使得 API 隐式的并且可以引起错误。取而代之的是,这些值应该作为参数传递。
  5. 不要将 context 存储在结构中,在函数中显式传递它们,最好是作为第一个参数。
  6. 永远不要传递不存在的 context 。相反,如果您不确定使用什么,使用一个 ToDo context。
  7. Context 结构没有取消方法,因为只有派生 context 的函数才应该取消 context。

https://studygolang.com/articles/13866?fr=sidebar
via: https://medium.com/@parikshit/understanding-the-context-package-in-golang-b1392c821d14
作者:Parikshit Agnihotry 译者:themoonbear 校对:polaris1119