go - Go 上下文取消函数的最佳实践

标签 go go-context

我一直在阅读一些关于使用golang的context包的文章。我最近在博客中看到以下文章:http://p.agnihotry.com/post/understanding_the_context_package_in_golang/

文章对 go 中的上下文取消函数进行了以下说明:

"You can pass around the cancel function if you wanted to, but, that is highly not recommended. This can lead to the invoker of cancel not realizing what the downstream impact of canceling the context may be. There may be other contexts that are derived from this which may cause the program to behave in an unexpected fashion. In short, NEVER pass around the cancel function."

但是,如果我希望激活父 context.Done() channel ,将取消函数作为参数传递似乎是唯一的选择(请参阅下面的代码片段)。例如,下面代码片段中的代码 Done channel 仅在执行 function2 时激活。

package main

import (
    "context"
    "fmt"
    "time"
)

func function1(ctx context.Context) {
    _, cancelFunction := context.WithCancel(ctx)
    fmt.Println("cancel called from function1")
    cancelFunction()
}

func function2(ctx context.Context, cancelFunction context.CancelFunc) {
    fmt.Println("cancel called from function2")
    cancelFunction()
}

func main() {
    //Make a background context
    ctx := context.Background()
    //Derive a context with cancel
    ctxWithCancel, cancelFunction := context.WithCancel(ctx)

    go function1(ctxWithCancel)
    time.Sleep(5 * time.Second)

    go function2(ctxWithCancel, cancelFunction)

    time.Sleep(5 * time.Second)

    // Done signal is only received when function2 is called
    <-ctxWithCancel.Done()
    fmt.Println("Done")
}

那么,传递这个取消函数实际上是一个问题吗?是否有与使用 context 包及其取消功能相关的最佳实践?

最佳答案

在您的具体示例中,代码量足够小,理解其工作原理可能没有问题。当您替换function1时,问题就开始了。和function2与更复杂的事情。您链接到的文章给出了为什么传递取消上下文可以做一些难以推理的事情的具体原因,但更一般的原则是您应该尝试将协调工作(取消、旋转 goroutine)从底层工作中分离出来尽可能多地完成(无论 function1function2 正在做什么)。这只会有助于更轻松地独立推理代码的子部分,并有助于使测试变得更容易。 “function2 does ”比“function2 does 并且与 function1 协调”更容易理解。

而不是将取消函数传递给function2 ,你可以在你生成的 goroutune 中调用它来运行 function2 :

func main() {
  //...
  go func() {
    function2(ctxWithCancel)
    cancelFunction()
  }()
  //...
}

这是侄女,因为确定何时取消的协调工作全部包含在调用函数中,而不是分散在多个函数中。


如果你想拥有function2有条件地取消上下文,让它显式返回某种值来指示是否发生了某些可取消的条件:

func function2(ctx context.Context) bool {
  //...
  if workShouldBecanceled() {
    return true
  }
  //...
  return false
}

func main() {
  //...
  go func() {
    if function2(ctxWithCancel) {
      cancelFunction()
    }
  }()
  //...
}

这里我使用了一个 bool 值,但这种模式通常与 error 一起使用。 s - 如果function2返回非零 error ,取消剩下的工作。

根据您在做什么,类似 errgroup.WithContext 可能对你有用。这可以协调多个并发操作,所有这些操作都可能失败,并在第一个操作失败后立即取消其他操作。


我在上下文取消方面尝试遵循的另一个最佳实践:始终确保在某个时刻调用取消函数。来自 docs ,调用取消函数两次是安全的,所以我经常这样做:

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  defer cancel()
  //...
  if shouldCancel() {
    cancel()
  }
  //...
}

编辑回复评论:

如果您有多个长时间运行的操作(例如服务器、连接等),并且您希望在第一个操作停止后立即关闭所有操作,则上下文取消是一种合理的方法去做。但是,我仍然建议您在单个函数中处理所有上下文交互。像这样的事情会起作用:

func operation1(ctx context.Context) {
   for {
     select {
     case <-ctx.Done():
       return
     default:
     }
     //...
   }
}

func operation2(ctx context.Context) {
  // Similar code to operatoin1()
}

func main() {
  ctx, cancel := context.WithCancel(context.Background())
  var wg sync.WaitGroup
  wg.Add(2)
  go func() {
    defer wg.Done()
    defer cancel()
    operation1(ctx)
  }()
  go func() {
    defer wg.Done()
    defer cancel()
    operation2(ctx)
  }()
  wg.Wait()
}

一旦其中一个操作终止,另一个操作就会被取消,但是 main仍然等待两者完成。两个操作都不需要担心管理这个问题。

关于go - Go 上下文取消函数的最佳实践,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70042646/

相关文章:

selenium - 端口不可用。退出...对于 golang selenium webdriver

go - 如何在运行 go mod download 时强制使用特定的软件包版本?

go - 查询数据库时如何将雪花数组转换为Golang中的数组

go - 将上下文与取消一起使用,Go 例程不会终止

go - context.TODO() 或 context.Background(),我更喜欢哪一个?

xml - 使用go提取xml属性

将数组解包为参数

go - context.TODO() 或 context.Background(),我更喜欢哪一个?

go - 如何将上下文值从 Gin 中间件传播到 gqlgen 解析器?