WaitGroup で goroutine の終了を待つ

複数の goroutine を起動したとき、すべてが終わるまで待ちたいことがあります。channel でも実現できますが、sync.WaitGroup を使うとよりシンプルに書けます。

WaitGroup の基本

WaitGroup は「待つべき goroutine の数」をカウントします。goroutine を起動する前にカウントを増やし、終了したらカウントを減らします。カウントが 0 になるまで待機できます。

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    wg.Add(1)
    go func() {
        defer wg.Done()
        fmt.Println("goroutine 完了")
    }()

    wg.Wait()
    fmt.Println("すべて完了")
}

Add(1) でカウントを 1 増やし、Done() で 1 減らします。Wait() はカウントが 0 になるまでブロックします。

3 つのメソッド

WaitGroup には 3 つのメソッドがあります。

Add(n)

カウントを n 増やす。goroutine を起動する前に呼ぶ。

Done()

カウントを 1 減らす。goroutine の終了時に呼ぶ。defer と組み合わせることが多い。

Wait()

カウントが 0 になるまでブロックする。

Done()Add(-1) と同じですが、意図が明確になるので Done を使うのが一般的です。

複数の goroutine を待つ

複数の goroutine を起動して、すべてを待つ例です。

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d 開始\n", id)
    time.Sleep(time.Duration(id) * 100 * time.Millisecond)
    fmt.Printf("Worker %d 完了\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("すべての Worker が完了")
}

WaitGroup を関数に渡すときは、ポインタで渡します。値渡しにするとコピーされてしまい、正しく動作しません。

Add はループの外でもよい

goroutine の数がわかっているなら、まとめて Add することもできます。

wg.Add(3)
for i := 1; i <= 3; i++ {
    go worker(i, &wg)
}

ただし、Add を goroutine の中で呼ぶのは避けてください。Wait がすでに呼ばれた後に Add すると panic になります。

defer wg.Done() を忘れずに

Done を呼び忘れると、Wait が永遠にブロックします。defer wg.Done() を goroutine の最初に書く癖をつけておくと安全です。

go func() {
    defer wg.Done()
    // 処理
}()

defer を使えば、途中で return や panic が起きても Done が確実に呼ばれます。