Golang通过goroutine使生活变得更容易,然而,有时很难有效地处理发生在goroutine内部的错误。例如,想象一下,你有一个由某种动作组成的数组,并想在其中的每一个动作上运行一个特定的函数。另一方面,在出现错误的情况下,你想把这个错误传播给上级函数。
让我们用代码来解释一下,想象一下,我们有一组runners ,每个运行器都有一个Handle 的函数:
type HandlerFunc func(input string) error
type Runner struct {
Name string
Handle HandlerFunc
}
我还喜欢定义另一种类型,名为Runners 。它只是一个简单的包装器,围绕着运行器的数组:
type Runners []Runner
因此,我可以定义一个函数,像这样贯穿所有的runner:
func (r Runners) Execute() error {
for _, runner := range r {
if err := runner.Handle(runner.Name); err != nil {
return err
}
}
return nil
}
最后,定义一些运行器并执行它们:
func main() {
runners := Runners{
Runner{
Name: "1",
Handle: func(input string) error {
fmt.Printf("runner %s is running\n", input)
return nil
},
},
Runner{
Name: "2",
Handle: func(input string) error {
return fmt.Errorf("something bad happened in runner [%s]", input)
},
},
Runner{
Name: "3",
Handle: func(input string) error {
fmt.Printf("runner %s is running\n", input)
return nil
},
},
}
err := runners.Execute()
if err != nil {
fmt.Printf("execution failed: %v", err)
}
}
通过运行这段代码,我得到这样的输出:runner 1 is running 。问题是我们没有运行第三个运行器。所以在这种情况下,我们打印错误并继续运行Execution,但我们不能将错误传播到更高的函数中去!
func (r Runners) Execute() error {
for _, runner := range r {
if err := runner.Handle(runner.Name); err != nil {
fmt.Printf("error happened in runner [%s]: %v", runner.Name, err)
}
}
return nil
}
现在,如果我们想在一个不同的goroutine中运行每个runner,并有完全相同的场景,该怎么办?让我们稍微改变一下代码,看看它是否有效。
首先,我们在函数调用后面添加一个go。所以我们的Execute 函数应该是这样的:
func (r Runners) Execute() error {
for _, runner := range r {
go func(runner Runner) error {
if err := runner.Handle(runner.Name); err != nil {
return err
}
return nil
}(runner)
}
return nil
}
然而,正如你可能知道的,如果我们再次运行这个程序,将没有任何输出,因为我们从来没有说过应用程序要等到goroutines完成工作。为了简单起见,让我们只在main 函数中添加一个Sleep:
err := runners.Execute()
time.Sleep(3 * time.Second)
if err != nil {
fmt.Printf("execution failed: %v", err)
}
现在,让我们再次运行它。这一次的输出应该是这样的:
runner 1 is running
runner 3 is running
好的,这不是好的!执行的效果很好,因为我们可以执行运行者1和3,然而,我们仍然没有对错误做任何处理。
欢迎来到errgroup
现在,是时候用errgroup来解决这个问题了。它真的很简单,很容易。它的工作原理类似于同步包下的waitgroups。老实说,它是在幕后使用等待组,但由于所提到的情况很常见,errgroup使我们的生活变得更简单
如果你熟悉等待组,你应该知道wg.Done() 和wg.Wait() 是什么意思。errgroup 提供同样的东西。让我们用代码把事情说清楚。首先,我们声明一个g 变量:
g := new(errgroup.Group)
并在一个带有Go func的goroutine内运行我们的execute函数。因此我们的Execute 函数变成了这样:
func (r Runners) Execute() {
for _, runner := range r {
rx := runner
g.Go(func() error {
return rx.Handle(rx.Name)
})
}
}
Go 函数给出和func ,返回一个错误,如果这个错误不是nil ,你将在Wait 函数的返回中看到。
这里是Wait 的函数:
runners.Execute()
err := g.Wait()
if err != nil {
fmt.Printf("execution failed: %v", err)
}
正如你所看到的,我们不再需要time.Sleep() ,因为Wait func,等待直到最后一个goroutine得到结果并返回第一个发生的错误。如果所有函数的运行都没有错误,它就会返回nil的输出是类似的:
runner 3 is running
runner 1 is running
execution failed: something bad happened in runner [2]
这里是完整的代码:
package main
import (
"fmt"
"golang.org/x/sync/errgroup"
)
var g errgroup.Group
type HandlerFunc func(input string) error
type Runner struct {
Name string
Handle HandlerFunc
}
type Runners []Runner
func (r Runners) Execute() {
for _, runner := range r {
rx := runner
g.Go(func() error {
return rx.Handle(rx.Name)
})
}
}
func main() {
runners := Runners{
Runner{
Name: "1",
Handle: func(input string) error {
fmt.Printf("runner %s is running\n", input)
return nil
},
},
Runner{
Name: "2",
Handle: func(input string) error {
return fmt.Errorf("something bad happened in runner [%s]", input)
},
},
Runner{
Name: "3",
Handle: func(input string) error {
fmt.Printf("runner %s is running\n", input)
return nil
},
},
}
runners.Execute()
err := g.Wait()
if err != nil {
fmt.Printf("execution failed: %v", err)
}
}