Go 言語では、メインゴルーチン(main goroutine)が他のゴルーチンの完了を待ってから処理を続行したりプログラムを終了したりする必要があるのは、よくある並行同期の要件です。Go はこれを実現するためにいくつかの仕組みを提供しており、シーンやニーズによって使い分けます。
方法 1:sync.WaitGroup の使用
sync.WaitGroup は、Go で最もよく使われる同期ツールで、一連のゴルーチンの完了を待つために使用します。カウンターメカニズムによって動作し、メインゴルーチンが複数の子ゴルーチンを待つ場合に非常に適しています。
サンプルコード
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// 3つのゴルーチンを起動
for i := 1; i <= 3; i++ {
wg.Add(1) // カウンターを1増やす
go func(id int) {
defer wg.Done() // タスク完了後カウンターを1減らす
fmt.Printf("Goroutine %d is running\n", id)
}(i)
}
wg.Wait() // メインゴルーチンがすべてのゴルーチンの完了を待つ
fmt.Println("All goroutines finished")
}
出力(順序は異なる場合があります):
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished
動作原理:
- wg.Add(n):カウンターを増やし、待つべきゴルーチンの数を示す。
- wg.Done():各ゴルーチンの完了時に呼び出され、カウンターを 1 減らす。
- wg.Wait():カウンターがゼロになるまでメインゴルーチンをブロックする。
メリット:
- シンプルで使いやすく、固定数のゴルーチンに最適。
- 追加のチャネルが不要で、パフォーマンスオーバーヘッドが小さい。
方法 2:チャネル(Channel)の使用
チャネルを使ってシグナルを送信することで、メインゴルーチンがすべてのゴルーチンから完了シグナルを受け取るまで待つことができます。この方法はより柔軟ですが、通常は WaitGroup よりやや複雑です。
サンプルコード
package main
import "fmt"
func main() {
done := make(chan struct{}) // 完了通知用のシグナルチャネル
numGoroutines := 3
for i := 1; i <= numGoroutines; i++ {
go func(id int) {
fmt.Printf("Goroutine %d is running\n", id)
done <- struct{}{} // タスク完了後にシグナルを送信
}(i)
}
// すべてのゴルーチンの完了を待つ
for i := 0; i < numGoroutines; i++ {
<-done // シグナルを受信
}
fmt.Println("All goroutines finished")
}
出力(順序は異なる場合があります):
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished
動作原理:
- 各ゴルーチンが完了時に done チャネルへシグナルを送信します。
- メインゴルーチンは決められた回数だけシグナルを受信し、全タスク完了を確認します。
メリット:
- 柔軟性が高く、データ(タスクの結果など)を運ぶこともできます。
- ゴルーチンの数が動的に変わる場合にも適しています。
デメリット:
- 受信回数の管理が必要で、ややコードが煩雑になります。
方法 3:context と組み合わせて終了を制御
context.Context を使用すると、ゴルーチンの終了をエレガントに制御でき、すべてのタスクの完了をメインゴルーチンで待つことも可能です。この方法は、キャンセルやタイムアウトが必要なシーンに特に適しています。
サンプルコード
package main
import (
"context"
"fmt"
"sync"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
select {
case <-ctx.Done():
fmt.Printf("Goroutine %d cancelled\n", id)
return
default:
fmt.Printf("Goroutine %d is running\n", id)
}
}(i)
}
// タスク完了のシミュレーション
cancel() // キャンセルシグナルを送信
wg.Wait() // すべてのゴルーチンの終了を待つ
fmt.Println("All goroutines finished")
}
出力(キャンセルのタイミングによって異なる場合あり):
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished
動作原理:
- context はゴルーチンに終了を通知します。
- WaitGroup でメインゴルーチンがすべてのゴルーチンの完了を待ちます。
メリット:
- キャンセルやタイムアウトのサポートにより、複雑な並行処理に適します。
デメリット:
- コードの複雑さがやや増します。
方法 4:errgroup の利用(推奨)
golang.org/x/sync/errgroup は、WaitGroup の待機機能とエラー処理を組み合わせた高機能ツールです。一連のタスクを待ちつつ、エラーも適切に処理したい場合に特に適しています。
package main
import (
"fmt"
"golang.org/x/sync/errgroup"
)
func main() {
var g errgroup.Group
for i := 1; i <= 3; i++ {
id := i
g.Go(func() error {
fmt.Printf("Goroutine %d is running\n", id)
return nil // エラーなし
})
}
if err := g.Wait(); err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("All goroutines finished")
}
}
出力:
Goroutine 1 is running
Goroutine 2 is running
Goroutine 3 is running
All goroutines finished
動作原理:
- g.Go()でゴルーチンを起動し、待機グループに追加します。
- g.Wait()で、すべてのゴルーチンの完了を待ち、最初の非 nil エラーがあればそれを返します。
メリット:
- シンプルでエレガント、エラー伝搬もサポート。
- 組み込みのコンテキストサポート(errgroup.WithContext の利用も可)。
インストール方法:
-
go get golang.org/x/sync/errgroup
で追加する必要があります。
どの方法を選ぶべきか?
sync.WaitGroup
- 適用シーン:固定数のシンプルなタスク
- メリット:シンプルで効率的
- デメリット:エラーやキャンセルには非対応
チャネル
- 適用シーン:動的なタスクや結果を伝えたい場合
- メリット:高い柔軟性
- デメリット:手動管理がやや複雑
context
- 適用シーン:キャンセルやタイムアウトが必要な複雑なケース
- メリット:キャンセルやタイムアウトに対応
- デメリット:コードがやや複雑
errgroup
- 適用シーン:エラー処理と待機を両立したい現代的なアプリケーション
- メリット:エレガントで高機能
- デメリット:追加依存が必要
補足:なぜメインゴルーチンで直接 sleep しないのか?
time.Sleep は固定の遅延を入れるだけで、タスクの完了を正確に待つことができず、早すぎる終了や不要な待機を招く可能性があります。同期ツールの利用がより確実です。
まとめ
メインゴルーチンが他のゴルーチンを待つ最も一般的な方法は sync.WaitGroup で、シンプルかつ効率的です。エラー処理やキャンセル機能が必要な場合は、errgroup や context の併用がおすすめです。具体的なニーズに合わせて適切なツールを選択し、プログラムのロジックを明確かつリークのないものにしましょう。
私たちはLeapcell、Goプロジェクトのホスティングの最適解です。
Leapcellは、Webホスティング、非同期タスク、Redis向けの次世代サーバーレスプラットフォームです:
複数言語サポート
- Node.js、Python、Go、Rustで開発できます。
無制限のプロジェクトデプロイ
- 使用量に応じて料金を支払い、リクエストがなければ料金は発生しません。
比類のないコスト効率
- 使用量に応じた支払い、アイドル時間は課金されません。
- 例: $25で6.94Mリクエスト、平均応答時間60ms。
洗練された開発者体験
- 直感的なUIで簡単に設定できます。
- 完全自動化されたCI/CDパイプラインとGitOps統合。
- 実行可能なインサイトのためのリアルタイムのメトリクスとログ。
簡単なスケーラビリティと高パフォーマンス
- 高い同時実行性を容易に処理するためのオートスケーリング。
- ゼロ運用オーバーヘッド — 構築に集中できます。
Xでフォローする:@LeapcellHQ