用errgroup处理goroutines中的错误(附代码示例)

63 阅读4分钟

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)
    }
}