Go の errgroup で goroutine のエラーをまとめて扱う
複数の goroutine を起動して、どれか 1 つでもエラーが発生したら全体を中断したい場面は多いです。標準の sync.WaitGroup だけではエラーの収集や伝播が難しいため、golang.org/x/sync/errgroup パッケージが便利です。
errgroup の基本
errgroup は WaitGroup に似ていますが、エラーを返すことができます。
package main
import (
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
var g errgroup.Group
g.Go(func() error {
fmt.Println("タスク 1 実行")
return nil
})
g.Go(func() error {
fmt.Println("タスク 2 実行")
return nil
})
if err := g.Wait(); err != nil {
fmt.Println("エラー:", err)
} else {
fmt.Println("すべて成功")
}
}g.Go で goroutine を起動し、g.Wait ですべての完了を待ちます。いずれかがエラーを返すと、Wait はそのエラーを返します。
エラーが発生した場合
どれか 1 つでもエラーを返すと、Wait はそのエラーを返します。
func main() {
var g errgroup.Group
g.Go(func() error {
return nil
})
g.Go(func() error {
return fmt.Errorf("タスク 2 で失敗")
})
g.Go(func() error {
return nil
})
if err := g.Wait(); err != nil {
fmt.Println("エラー:", err) // タスク 2 で失敗
}
}複数のエラーが発生した場合、最初のエラーだけが返されます。すべてのエラーを収集したい場合は別の方法が必要です。
context との組み合わせ
errgroup.WithContext を使うと、エラー発生時に他の goroutine をキャンセルできます。
func main() {
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
fmt.Println("タスク 1 キャンセル")
return ctx.Err()
default:
fmt.Println("タスク 1:", i)
time.Sleep(100 * time.Millisecond)
}
}
return nil
})
g.Go(func() error {
time.Sleep(300 * time.Millisecond)
return fmt.Errorf("タスク 2 でエラー発生")
})
if err := g.Wait(); err != nil {
fmt.Println("結果:", err)
}
}タスク 2 がエラーを返すと、ctx がキャンセルされ、タスク 1 もそれを検知して終了します。これにより、無駄な処理を早期に打ち切れます。
並列データ取得の例
複数の API からデータを並列取得する場面を考えてみましょう。
type Result struct {
Users []string
Products []string
}
func fetchData() (*Result, error) {
var result Result
var mu sync.Mutex
g, ctx := errgroup.WithContext(context.Background())
g.Go(func() error {
// ユーザー API を呼び出し
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(100 * time.Millisecond):
mu.Lock()
result.Users = []string{"Alice", "Bob"}
mu.Unlock()
return nil
}
})
g.Go(func() error {
// 商品 API を呼び出し
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(150 * time.Millisecond):
mu.Lock()
result.Products = []string{"iPhone", "MacBook"}
mu.Unlock()
return nil
}
})
if err := g.Wait(); err != nil {
return nil, err
}
return &result, nil
}両方の API 呼び出しが成功すれば結果を返し、どちらかが失敗すれば即座に中断してエラーを返します。
同時実行数の制限
Go 1.20 以降では SetLimit で同時実行数を制限できます。
func main() {
g := new(errgroup.Group)
g.SetLimit(3) // 同時に 3 つまで
for i := 0; i < 10; i++ {
i := i
g.Go(func() error {
fmt.Printf("タスク %d 開始\n", i)
time.Sleep(500 * time.Millisecond)
fmt.Printf("タスク %d 完了\n", i)
return nil
})
}
g.Wait()
}10 個のタスクがありますが、同時に実行されるのは 3 つまでです。API のレート制限を守りたい場合などに便利です。
WaitGroup との比較
errgroup と WaitGroup の違いを整理します。
エラーを返す仕組みがない。単純に goroutine の完了を待つだけ。
エラーを収集・伝播できる。context と連携してキャンセルも可能。SetLimit で同時実行数も制御できる。
エラーハンドリングが必要な並行処理には errgroup を使うのがおすすめです。標準ライブラリではありませんが、準標準として広く使われています。インストールは go get golang.org/x/sync/errgroup で行えます。