学习GO语言中使用errgroup包的并发模式

511 阅读4分钟

errgroup 中有什么?

errorgroup 包不是标准库的一部分,它是一个第三方的包,必须下载(使用典型的 go mod调用)才能在我们的任何Go项目中使用,它在某种程度上得到了Go团队的支持,因为包的完整名称是 golang.org/x/sync/errgroup,但它仍然没有遵循第一版的兼容性

这个包包括同步错误传播上下文取消;它的目的是用于在一个共同任务上工作的goroutines组。

比较errgroupsync.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已经完成。
  • L30-33:启动一个goroutine等待所有其他goroutine,这是调用WaitGroup中的Wait 方法。
    • L32: 一旦我们被告知所有的goroutine都完成了,我们就关闭返回的通道ch ,这是为了告诉调用者一切都已完成。
  • 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 类型启动的,并且
  • L29errgroup.Wait 是用来等待启动的goroutines完成的。

让我们看看另一个例子,它真正显示了使用errgroup 的力量。

使用context.Contexterrgroup

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:我们使用一个forselect ,以确定使用的通道是否仍然有效。
    • L23-26:判断上下文变量(ctx)是否已经完成,以及
    • L27-32:处理从通道收到的消息,对它被关闭的时间做出反应。

我之前使用这种模式实现的一个实际例子是,当我涉及到 实现复杂的流水线,它展示了在使用goroutines构建复杂的程序时,所有这些是如何结合在一起的,请随时阅读该系列文章,它应该会给你提供更多关于一个具体而完整的例子的细节。

总结

我是errgroup 的超级粉丝,因为它简化了构建需要同步工作的程序的过程,而且它允许我们处理在多个goroutine之间传播的错误。