errgroup 中有什么?
errorgroup 包不是标准库的一部分,它是一个第三方的包,必须下载(使用典型的 go mod调用)才能在我们的任何Go项目中使用,它在某种程度上得到了Go团队的支持,因为包的完整名称是 golang.org/x/sync/errgroup,但它仍然没有遵循第一版的兼容性。
这个包包括同步、错误传播和上下文取消;它的目的是用于在一个共同任务上工作的goroutines组。
比较errgroup 和sync.WaitGroup
在某种程度上,errgroup 与使用sync.WaitGroup 有点类似,因为它在某种程度上允许我们等待所有的goroutines完成,然而最大的区别是当它与context.Context 类型一起使用时。
让我们来看看下面的例子。
1func main() {
2 wait := waitGroups()
3 <-wait
4}
5
6func waitGroups() <-chan struct{} {
7 ch := make(chan struct{}, 1)
8
9 var wg sync.WaitGroup
10
11 for _, file := range []string{"file1.csv", "file2.csv", "file3.csv"} {
12 file := file // XXX: Important gotcha when using variables in goroutines in the same block
13
14 wg.Add(1)
15
16 go func() {
17 defer wg.Done()
18
19 ch, err := read(file)
20 if err != nil {
21 fmt.Printf("error reading %v\n", err)
22 }
23
24 for line := range ch {
25 fmt.Println(line)
26 }
27 }()
28 }
29
30 go func() {
31 wg.Wait()
32 close(ch)
33 }()
34
35 return ch
36}
上面的代码使用了一个叫做read 的函数来读取输入中标明的CSV文件,在这个例子中最重要的是那个read 函数的返回值,以及这些值如何在下面的步骤中使用。
- L14:我们每次读取一个文件名时,都会将
WaitGroup增加到1。 - L16-27:对于我们应该读取的每个文件名,我们都要启动一个新的goroutine。
- L19: 这个新的goroutine将从返回的通道中读取数值,
read。 - L17: 一旦通道关闭,我们在WaitGroup中调用
Done,以表示goroutine已经完成。
- L19: 这个新的goroutine将从返回的通道中读取数值,
- L30-33:启动一个goroutine来等待所有其他goroutine,这是调用WaitGroup中的
Wait方法。- L32: 一旦我们被告知所有的goroutine都完成了,我们就关闭返回的通道
ch,这是为了告诉调用者一切都已完成。
- L32: 一旦我们被告知所有的goroutine都完成了,我们就关闭返回的通道
- L35: 我们返回一个通道
ch,用来指示原来的调用者(本例中为main)等待,直到一切完成。
上面的代码肯定是有效的,让我们看看如何用errgroup 来实现。
1func main() {
2 wait := errGroup()
3 <-wait
4}
5
6func errGroup() <-chan struct{} {
7 ch := make(chan struct{}, 1)
8
9 var g errgroup.Group
10
11 for _, file := range []string{"file1.csv", "file2.csv", "file3.csv"} {
12 file := file
13
14 g.Go(func() error {
15 ch, err := read(file)
16 if err != nil {
17 return fmt.Errorf("error reading %w", err)
18 }
19
20 for line := range ch {
21 fmt.Println(line)
22 }
23
24 return nil
25 })
26 }
27
28 go func() {
29 if err := g.Wait(); err != nil {
30 fmt.Printf("Error reading files %v", err)
31 }
32
33 close(ch)
34 }()
35
36 return ch
37}
与WaitGroup 的例子相比,最大的区别将是这样的事实。
- L9:我们使用
errgroup.Group。 - L14:goroutines是使用
Go方法从errgroup.Group类型启动的,并且 - L29:
errgroup.Wait是用来等待启动的goroutines完成的。
让我们看看另一个例子,它真正显示了使用errgroup 的力量。
使用context.Context 与errgroup
errgroup 包在包含context.Context 时大放异彩,记得context.Context 是用来做取消和错误传播等事情的,把它和errgroup 结合起来,你应该可以建立复杂的程序,可以对作为context.Context 类型的一部分触发的错误做出反应,比如说。
1func main() {
2 ctx := context.Background()
3 wait := errGroup(ctx)
4 <-wait
5}
6
7func errGroup(ctx context.Context) <-chan struct{} {
8 ch := make(chan struct{}, 1)
9
10 g, ctx := errgroup.WithContext(ctx)
11
12 for _, file := range []string{"file1.csv", "file2.csv", "file3.csv"} {
13 file := file
14
15 g.Go(func() error {
16 ch, err := read(file)
17 if err != nil {
18 return fmt.Errorf("error reading %w", err)
19 }
20
21 for {
22 select {
23 case <-ctx.Done():
24 fmt.Printf("Context completed %v\n", ctx.Err())
25
26 return ctx.Err()
27 case line, ok := <-ch:
28 if !ok {
29 return nil
30 }
31
32 fmt.Println(line)
33 }
34 }
35 })
36 }
37
38 go func() {
39 if err := g.Wait(); err != nil {
40 fmt.Printf("Error reading files: %v", err)
41 }
42
43 close(ch)
44 }()
45
46 return ch
47}
这个例子是我们之前介绍errgroup.Group 时的修改版,关键部分是。
- L21-34:我们使用一个
for,select,以确定使用的通道是否仍然有效。- L23-26:判断上下文变量(
ctx)是否已经完成,以及 - L27-32:处理从通道收到的消息,对它被关闭的时间做出反应。
- L23-26:判断上下文变量(
我之前使用这种模式实现的一个实际例子是,当我涉及到 实现复杂的流水线,它展示了在使用goroutines构建复杂的程序时,所有这些是如何结合在一起的,请随时阅读该系列文章,它应该会给你提供更多关于一个具体而完整的例子的细节。
总结
我是errgroup 的超级粉丝,因为它简化了构建需要同步工作的程序的过程,而且它允许我们处理在多个goroutine之间传播的错误。