フューチャー技術ブログ

Go1.25リリース連載:sync

はじめに

製造エネルギー事業部の辻です。Go1.25 リリース連載 の3本目です。

マイナーアップデートから sync パッケージを取り上げて紹介します。

アップデートサマリ

The new method on WaitGroup, WaitGroup.Go, makes the common pattern of creating and counting goroutines more convenient.

https://go.dev/doc/go1.25#syncpkgsync

sync パッケージに新しく WaitGroup.Go() というメソッドが追加されました。ゴルーチンの生成と完了を集計する一般的なパターンを便利に記述できるようになりました。

このメソッドを利用すると、たとえば以下のような並行処理のコードをより簡潔に記述できるようになります。

Go1.24以前
	var wg sync.WaitGroup
tasks := []string{"task 1", "task 2", "task 3"}

for _, t := range tasks {
// 1. ゴルーチン起動前にカウンタをインクリメント
wg.Add(1)

go func(task string) {
// 2. ゴルーチン終了時にカウンタをデクリメント
defer wg.Done()
fmt.Printf("Executing %s\n", task)
}(t)
}

wg.Wait()
fmt.Println("All tasks completed.")
}
Go1.25
var wg sync.WaitGroup
tasks := []string{"task 1", "task 2", "task 3"}

for _, t := range tasks {
// wg.Go() を呼び出すだけでOK
wg.Go(func() {
fmt.Printf("Executing %s\n", t)
})
}

wg.Wait()
fmt.Println("All tasks completed.")

なお、go vet の静的解析で sync.WaitGroup.Add() のミスパターンの検知ができるようになりました。こちらは真野さんの記事で解説予定ですので本記事では割愛します。

背景など

関連するIssueは #63796:sync: add WaitGroup.Go です。

Go の並行処理の実装パターンとして以下のようなコードがよく見られます。

var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
// i := i この変数の確保は Go1.22 で不要になっています
wg.Add(1)
go func() {
defer wg.Done()
work(i)
}()
}
wg.Wait()

実装として wg.Add(1)defer wg.Done() は必要ですが、忘れがちなミスパターンの一つでもあります。

なおGo1.21以前はループ変数をクロージャで扱う際にコード内で明示的に i := i などしてメモリ確保が必要でしたが、Go1.22以降はGo側でよしなにメモリを確保できるようになっており、アプリ開発者が i := i とする実装は不要になっています。

今回のリリースされる WaitGroup.Go() メソッドで、上記のようなパターンをカプセル化し、Goの並行処理をより簡潔に実装できるようになります。以下が WaitGroup.Go() メソッドの実装です。

func (wg *WaitGroup) Go(f func()) {
wg.Add(1)
go func() {
defer wg.Done()
f()
}()
}

https://cs.opensource.google/go/go/+/release-branch.go1.25:src/sync/waitgroup.go;l=235-241

errgroup との違いは?

Goの並行処理の実装をするときに準標準ライブラリである golang.org/x/sync/errgroup パッケージを利用されている方もいるのではないか、と思います。私も errgroup にはよくお世話になっています。

errgroup パッケージは標準ライブラリである sync パッケージに依存しています。sync パッケージはより低レイヤーなパッケージと言えますが、並行処理を扱う上で以下のような機能的な違いがあります。

特徴 sync.WaitGroup (と Go メソッド) golang.org/x/sync/errgroup
主な目的 全てのゴルーチンの完了を待機 全てのゴルーチンの完了を待機 + 最初のエラーを伝播 + キャンセル
エラー処理 Goメソッドには存在しない。必要に応じて個々のゴルーチンで実装、呼び出し元との連携はチャネル等を利用 最初のエラーを自動的に捕捉・伝播
キャンセル なし context.Context と連携し、エラー発生時に他のゴルーチンをキャンセル
同時実行数制御 チャネル等で実装が必要 SetLimit() で指定可能

上記からもわかるように今回導入される WaitGroup.Go() メソッドが既存の errgroup パッケージを代替するものか?というとそうではなさそうです。

高機能なAPIを提供するという意味で errgroup パッケージは今後も使われるのではないか、と思います。一方、シンプルな機能で十分な場合や、ゴルーチンで発生するすべてのエラーを収集したい場合は WaitGroup.Go() とチャネルを利用した実装が必要でしょう。

まとめ

Go 1.25で sync パッケージの WaitGroup.Go() メソッドを紹介しました。

エラーハンドリングやコンテキストの伝播、同時実行数制御が必要な場合では引き続き errgroup が便利ですが、シンプルな処理であれば WaitGroup.Go() が有用になるでしょう。