Goで並行実行した処理のエラーを検知したい時は、errgroupを使用しよう

Go言語

ゴルーチンを使用して並行実行した後に、一度でもエラーがあれば、検知してハンドリングしたいと思います。
この記事ではどうすれば、この問題を解決できるかを紹介したいと思います。
解決策として、errgroupを使用します。
こちらを使用することで、ゴルーチンで並行実行している処理が1つでもエラーになった場合に、エラーを検知できるようになります。

コード

10回ループしてiを0からインクリメントして、iがコマンドライン引数と同じ値の場合に、エラーになる処理をGorutineで並行実行するようにしている。

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strconv"
	"time"

	"golang.org/x/sync/errgroup"
)

var NUM int

func printInt(num int) error {
	log.Println(num)
	if num == NUM {
		return fmt.Errorf("Fail")
	}
	time.Sleep(3 * time.Second)
	return nil
}

func egGo(eg *errgroup.Group, value int) {
	eg.Go(func() error {
		return printInt(value)
	})
}

func main() {
	NUM, _ = strconv.Atoi(os.Args[1])
	eg, _ := errgroup.WithContext(context.TODO())

	for i := 0; i < 10; i++ {
		value := i
		egGo(eg, value)
	}
	err := eg.Wait()
	log.Println("Complete all task", err)
}

実行結果1(エラーが発生するパターン)

% go run main.go 1
2022/12/10 19:15:18 3
2022/12/10 19:15:18 7
2022/12/10 19:15:18 9
2022/12/10 19:15:18 1
2022/12/10 19:15:18 0
2022/12/10 19:15:18 8
2022/12/10 19:15:18 2
2022/12/10 19:15:18 4
2022/12/10 19:15:18 6
2022/12/10 19:15:18 5
2022/12/10 19:15:21 Complete all task Fail

1でエラーになるため、「Fail」が最後出力されていることがわかる。

実行結果2(エラーが発生しないパターン)

 % go run main.go 20
2022/12/10 19:19:23 6
2022/12/10 19:19:23 0
2022/12/10 19:19:23 5
2022/12/10 19:19:23 2
2022/12/10 19:19:23 9
2022/12/10 19:19:23 4
2022/12/10 19:19:23 7
2022/12/10 19:19:23 8
2022/12/10 19:19:23 3
2022/12/10 19:19:23 1
2022/12/10 19:19:26 Complete all task <nil>

20ではエラーにならないので、先ほどFailだったのが<nil>になっている

注意点

複数のエラーが発生した場合は、最初エラーになった処理の結果が出力されるようになる。
なので並行実行する各処理の中で、エラーを出力するようにした方が良い。
例として、iが1 or 2の場合にエラーにする処理を書いて、エラーになった場合に「Fail:{エラーになった時のnum}」を返しています。

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"golang.org/x/sync/errgroup"
)

func printInt(num int) error {
	log.Println(num)
	if num == 1 || num == 2 {
		return fmt.Errorf("Fail:%d", num)
	}
	time.Sleep(3 * time.Second)
	return nil
}

func egGo(eg *errgroup.Group, value int) {
	eg.Go(func() error {
		return printInt(value)
	})
}

func main() {
	eg, _ := errgroup.WithContext(context.TODO())

	for i := 0; i < 10; i++ {
		value := i
		egGo(eg, value)
	}
	err := eg.Wait()
	log.Println("Complete all task", err)
}

実行結果

% go run main.go  
2023/03/09 08:22:26 0
2023/03/09 08:22:26 6
2023/03/09 08:22:26 5
2023/03/09 08:22:26 7
2023/03/09 08:22:26 8
2023/03/09 08:22:26 4
2023/03/09 08:22:26 1
2023/03/09 08:22:26 3
2023/03/09 08:22:26 2
2023/03/09 08:22:26 9
2023/03/09 08:22:29 Complete all task Fail:1

1が2より先に実行されているので結果が「Fail:1」となっている。

まとめ

各処理でエラーを出力する必要がありますが、処理を並行で実行して、全ての処理が完了するまで待ちたい場合は、sync.WaitGroupよりもerrgourpの方が、エラーのハンドリングを書かなくて済むので、使い勝手が良いと思いました。
Go言語について他にも、Goでmockを作成する方法や、selectの仕組みについての記事も書いてますので、そちらも見てもらえたらと思います。

【おすすめ記事のリンク】

コメント

タイトルとURLをコピーしました