WaitGroup と errgroup の違いを理解する
- 公開日
- カテゴリ:golang
- タグ:golang

Go で並行処理を行う場合によく登場するのが sync.WaitGroup
と errgroup
です。
名前は似ていますが、役割や使いどころには大きな違いがあります。
この記事では、WaitGroup, errgroup はそれぞれ何ができてどういった違いがあるのかを整理します。
contents
WaitGroup:完了を待つための基本ツール
sync.WaitGroup は標準ライブラリが提供するもっとも基本的な同期機構です。複数の goroutine を並行実行し、それらがすべて終わるまで待機することができます。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
i := i
wg.Go(func() {
fmt.Println("task", i, "done")
})
}
wg.Wait()
fmt.Println("all tasks finished")
}
上記のコードでは 3 つのタスクを並行実行し、すべての終了を待っています。
実行結果は次のようになります(実行順は毎回変わります)。
task 1 done
task 3 done
task 2 done
all tasks finished
このように WaitGroup は「終了を待つ」だけで、エラー処理などは持っていません。
エラーが出ても最後まで実行される
次に、WaitGroup で「タスクがエラーを返した場合」を見てみます。
package main
import (
"fmt"
"sync"
)
func doWork(id int) error {
if id == 2 {
return fmt.Errorf("task %d failed", id)
}
fmt.Println("task", id, "done")
return nil
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
i := i
wg.Go(func() {
if err := doWork(i); err != nil {
// 途中キャンセルはできないのでログに出すだけ
fmt.Println("error:", err)
}
})
}
wg.Wait()
fmt.Println("all tasks finished")
}
実行結果:
task 1 done
error: task 2 failed
task 3 done
all tasks finished
WaitGroup には「エラーを契機に途中で止める機能」がありません。エラーは拾ってログに出すことはできますが、全体の処理は最後まで実行されます。
つまり「全タスクをやり切る」用途に向いているのが WaitGroup です。
エラーを収集する場合
WaitGroup はエラー処理をサポートしていませんが、エラーチャネルを自前で用意することで「どのタスクで失敗したか」を収集することができます。
package main
import (
"fmt"
"sync"
)
func doWork(id int) error {
if id == 2 {
return fmt.Errorf("task %d failed", id)
}
fmt.Println("task", id, "done")
return nil
}
func main() {
var wg sync.WaitGroup
errs := make(chan error, 3)
for i := 1; i <= 3; i++ {
i := i
wg.Go(func() {
if err := doWork(i); err != nil {
errs <- err
}
})
}
wg.Wait()
close(errs)
for err := range errs {
fmt.Println("error:", err)
}
}
実行結果:
task 1 done
task 3 done
error: task 2 failed
このように WaitGroup だけではエラーを返せないため、必要であれば自前で集約処理を書く必要があります。
ただしこの方法では「全部のエラーを集められる」一方で、「途中キャンセル」はできません。
errgroup:エラーを扱える拡張版
一方で errgroup は、WaitGroup に「エラー処理機能」を追加した外部パッケージ(golang.org/x/sync/errgroup
)です。
基本の使い方は WaitGroup とよく似ていますが、Go
に渡す関数が error
を返せる点が異なります。
そして、Wait()
で最初に発生したエラーを返してくれます。
package main
import (
"fmt"
"golang.org/x/sync/errgroup"
)
func doWork(id int) error {
if id == 2 {
return fmt.Errorf("task %d failed", id)
}
fmt.Println("task", id, "done")
return nil
}
func main() {
var g errgroup.Group
for i := 1; i <= 3; i++ {
i := i
g.Go(func() error {
return doWork(i)
})
}
if err := g.Wait(); err != nil {
fmt.Println("[errgroup] error:", err)
}
}
実行結果:
task 1 done
task 3 done
[errgroup] error: task 2 failed
このように、WaitGroup と異なり 最初のエラーを呼び出し元に返すことができます。
ただし 複数のエラーが同時に起きても、返ってくるのは最初の 1 件だけ である点には注意が必要です。
WithContext による途中キャンセル
さらに errgroup.WithContext
を使うと、「1つのタスクが失敗したら他の処理をキャンセルする」ことができます。
内部的には context.Context
を利用しており、goroutine 側が ctx.Done()
をチェックすることで早期終了が可能です。
package main
import (
"context"
"errors"
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func doWork(ctx context.Context, id int) error {
// 処理前にキャンセル済みなら即終了
select {
case <-ctx.Done():
fmt.Println("task", id, "canceled (pre)")
return ctx.Err()
default:
}
// 疑似的な処理
time.Sleep(time.Duration(id) * 100 * time.Millisecond)
// 処理途中のキャンセルも確認
select {
case <-ctx.Done():
fmt.Println("task", id, "canceled (mid)")
return ctx.Err()
default:
}
// 業務エラーの例
if id == 2 {
return fmt.Errorf("task %d failed", id)
}
fmt.Println("task", id, "done")
return nil
}
func main() {
fmt.Println("[RUN] errgroup.WithContext (distinguish cancel vs failure)")
g, ctx := errgroup.WithContext(context.Background())
for i := 1; i <= 5; i++ {
i := i
g.Go(func() error {
return doWork(ctx, i)
})
}
if err := g.Wait(); err != nil {
switch {
case errors.Is(err, context.Canceled):
fmt.Println("[errgroup] canceled:", err) // 他タスク由来のキャンセル
case errors.Is(err, context.DeadlineExceeded):
fmt.Println("[errgroup] deadline exceeded:", err) // タイムアウト
default:
fmt.Println("[errgroup] failure:", err) // 業務エラー(例: task 2 failed)
}
} else {
fmt.Println("[errgroup] all tasks finished")
}
}
実行結果:
[RUN] errgroup.WithContext (distinguish cancel vs failure)
task 1 done
[errgroup] failure: task 2 failed
task 3 canceled (mid)
task 4 canceled (mid)
task 5 canceled (mid)
ここでは task 2
が失敗した時点で ctx
がキャンセルされ、残りのタスクは中断されます。
このように errgroup は「途中キャンセル」をサポートできる点が WaitGroup との大きな違いです。
WaitGroupとerrgroup 違いの整理
ここまでの違いを表にまとめると以下の通りです。
WaitGroup (sync) | errgroup (x/sync) | |
---|---|---|
提供元 | 標準ライブラリ sync | 外部パッケージ golang.org/x/sync/errgroup |
目的 | 複数処理の完了を待つだけ | 複数処理の完了待ち + エラー処理 |
エラー処理 | 自前でログや集約が必要 | 最初のエラーだけ Wait() が返す |
複数エラー | 自前で集約すれば可能 | 不可(1つだけ) |
途中キャンセル | できない | WithContext で可能(要 ctx チェック) |
まとめ
- WaitGroup は「複数の処理が全部終わるまで待つ」ことに特化しており、エラーは自前で処理する必要がある。
- errgroup は「最初のエラーを返す」ことができ、
WithContext
を使えば「失敗したら全体をキャンセル」する仕組みも実現できる。 - ただし errgroup が返すのは最初の 1 件のエラーだけ。複数エラーをすべて収集したい場合は WaitGroup + エラーチャネルなど自前の仕組みが必要になる。
用途によって使い分けると便利。
- 全部やり切る処理 → WaitGroup
- 失敗したら途中で止めたい処理 → errgroup
シナリオ | 適したツール |
---|---|
全タスク完了を待ちたい | WaitGroup |
全エラーを集めたい | WaitGroup + エラーチャネル |
1つでも失敗したら即エラーにしたい | errgroup |
失敗時に残り処理を止めたい | errgroup.WithContext |