1. Home
  2. golang
  3. WaitGroup と errgroup の違いを理解する

WaitGroup と errgroup の違いを理解する

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

Go で並行処理を行う場合によく登場するのが sync.WaitGrouperrgroup です。

名前は似ていますが、役割や使いどころには大きな違いがあります。

この記事では、WaitGroup, errgroup はそれぞれ何ができてどういった違いがあるのかを整理します。

contents

  1. WaitGroup:完了を待つための基本ツール
    1. エラーが出ても最後まで実行される
    2. エラーを収集する場合
  2. errgroup:エラーを扱える拡張版
    1. WithContext による途中キャンセル
  3. WaitGroupとerrgroup 違いの整理

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

Author

rito

  • Backend Engineer
  • Tokyo, Japan
  • PHP 5 技術者認定上級試験 認定者
  • 統計検定 3 級