0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Go 1.25] WaitGroup.Go()を使って既存コードを書き換える際の注意点

Posted at

この記事でわかること

  • Go 1.25で追加されたWaitGroup.Go()メソッドの使い方
  • 従来の並行処理コードからWaitGroup.Go()への移行方法と注意点
  • Go 1.22のループ変数仕様変更がもたらす安全性の向上
  • 実践的な移行パターンとコード例
  • 移行時のトラブルシューティング

対象読者

  • Goで並行処理を実装している開発者
  • Go 1.25へのアップグレードを検討している方
  • sync.WaitGroupを使った並行処理パターンを理解したい方

概要

Go 1.25で追加されたWaitGroup.Go()メソッドは、並行処理のコードをより簡潔で安全に書けるようにする重要な機能です。しかし、従来の書き方から移行する際には、関数シグネチャの違いやGo 1.22のループ変数の仕様変更を理解する必要があります。本記事では、実際の移行経験をもとに、安全に移行するための具体的な手順を解説します。


Go 1.25のWaitGroup.Go()メソッドとは

新機能の概要

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

公式ドキュメント: https://go.dev/doc/go1.25#syncpkgsync

従来の書き方

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)  // goroutine起動前にカウンタをインクリメント
    go func(i int) {
        defer wg.Done()  // goroutine終了時にカウンタをデクリメント
        fmt.Printf("Task %d\n", i)
    }(i)
}

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

Go 1.25での書き方

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Go(func() {  // Add()とDefer wg.Done()が不要
        fmt.Printf("Task %d\n", i)
    })
}

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

WaitGroup.Go()の内部実装

実際の実装は非常にシンプルです:

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

この実装から分かる通り、Go()メソッドは:

  1. 自動的にwg.Add(1)を呼び出す
  2. goroutineを起動
  3. goroutine内でdefer wg.Done()を設定
  4. 渡された関数fを実行

つまり、開発者が手動で書いていたボイラープレートコードを内部で処理してくれます。


移行時の重要な注意点

関数シグネチャの違い

WaitGroup.Go()メソッドのシグネチャは以下の通りです:

func (wg *WaitGroup) Go(f func())

重要: 引数として渡せるのは引数を取らない関数 func() のみです。

よくあるエラー

従来の書き方をそのまま置き換えようとすると、コンパイルエラーが発生します:

// ❌ コンパイルエラー
for i := 0; i < 5; i++ {
    wg.Go(func(i int) {  // func(int)は受け取れない
        fmt.Printf("Task %d\n", i)
    }(i))
}

エラーメッセージ例:

cannot use func(i int) literal (value of type func(int)) as func() value in argument to wg.Go

正しい移行方法

Go 1.22以降では、ループ変数を直接クロージャでキャプチャできます:

// ✅ 正しい書き方 (Go 1.22+)
for i := 0; i < 5; i++ {
    wg.Go(func() {  // 引数なし
        fmt.Printf("Task %d\n", i)  // 外側のiを参照
    })
}

Go 1.22のループ変数の変更が鍵

なぜ安全に移行できるのか

Go 1.25のWaitGroup.Go()への移行が安全に行えるのは、Go 1.22でループ変数の扱いが変更されたからです。

公式ドキュメント:

Go 1.21以前の問題

Go 1.21以前では、ループ変数は1つのメモリアドレスを使い回していました。

// Go 1.21以前
for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i)  // すべてのgoroutineが同じメモリアドレスを参照
    }()
}
time.Sleep(time.Second)
// 出力: 3 3 3 (すべて最後の値)

なぜこうなるのか?

タイムライン形式で説明します:

時間 →

メインスレッド:
[i=0][i=1][i=2][i=3, ループ終了]
0μs  1μs  2μs  3μs

メモリアドレス 0x100 の i の値:
0 → 1 → 2 → 3 → 3 → 3 →
                ↑
            ここでgoroutine実行開始

goroutine1: [起動待ち...] [実行: 3を出力]
goroutine2: [起動待ち...] [実行: 3を出力]
goroutine3: [起動待ち...] [実行: 3を出力]
           100μs        200μs

ポイント:

  1. ループは超高速(数マイクロ秒)で完了
  2. goroutineの起動には時間がかかる(数十〜数百マイクロ秒)
  3. goroutineが実行される頃には、ループが終わってiは最後の値になっている
  4. すべてのgoroutineが同じメモリアドレスを参照

Go 1.21以前の回避策

// 回避策1: 引数で値をコピー
for i := 0; i < 3; i++ {
    go func(i int) {  // 引数で受け取る
        fmt.Println(i)
    }(i)  // その時点の値をコピー
}

// 回避策2: 新しい変数を作成
for i := 0; i < 3; i++ {
    i := i  // シャドーイング
    go func() {
        fmt.Println(i)
    }()
}

Go 1.22以降の改善

Go 1.22では、各イテレーションで新しいメモリ領域を確保するように変更されました。

公式の説明:

Previously, the variables declared by a "for" loop were created once and updated by each iteration. In Go 1.22, each iteration of the loop creates new variables, to avoid accidental sharing bugs.

// Go 1.22以降
for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i)  // 各goroutineが独自のメモリを参照
    }()
}
time.Sleep(time.Second)
// 出力: 0 1 2 (正しい値)

メモリの違い

Go 1.21以前:

メモリアドレス 0x100 に変数 i を確保

イテレーション1: 0x100 の値を 0 に更新
イテレーション2: 0x100 の値を 1 に更新
イテレーション3: 0x100 の値を 2 に更新

→ すべてのgoroutineが 0x100 を参照

Go 1.22以降:

イテレーション1: 0x100 に変数 i を確保、値は 0
イテレーション2: 0x200 に変数 i を確保、値は 1
イテレーション3: 0x300 に変数 i を確保、値は 2

→ 各goroutineが別々のメモリアドレスを参照

実践的な移行パターン

パターン1: シンプルなループ

移行前 (Go 1.21以前の安全な書き方)

var wg sync.WaitGroup
urls := []string{"url1", "url2", "url3"}

for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        response, err := http.Get(u)
        if err != nil {
            log.Printf("Error fetching %s: %v", u, err)
            return
        }
        defer response.Body.Close()
        // 処理...
    }(url)
}

wg.Wait()

移行後 (Go 1.25 with Go 1.22+)

var wg sync.WaitGroup
urls := []string{"url1", "url2", "url3"}

for _, url := range urls {
    wg.Go(func() {
        response, err := http.Get(url)  // 直接参照
        if err != nil {
            log.Printf("Error fetching %s: %v", url, err)
            return
        }
        defer response.Body.Close()
        // 処理...
    })
}

wg.Wait()

パターン2: インデックスを使用する場合

移行前

var wg sync.WaitGroup
items := []Item{...}
results := make([]Result, len(items))

for i := range items {
    wg.Add(1)
    go func(idx int) {
        defer wg.Done()
        results[idx] = processItem(items[idx])
    }(i)
}

wg.Wait()

移行後

var wg sync.WaitGroup
items := []Item{...}
results := make([]Result, len(items))

for i := range items {
    wg.Go(func() {
        results[i] = processItem(items[i])  // iを直接参照
    })
}

wg.Wait()

注意が必要なケース

ケース1: Go 1.21以前をサポートする必要がある場合
→ 従来の書き方を維持してください

ケース2: wg.Done()を条件付きで呼ぶ場合
WaitGroup.Go()は常にDone()を呼ぶため、条件分岐がある場合は注意

// ❌ このパターンは移行できない
wg.Add(1)
go func() {
    if someCondition {
        defer wg.Done()
        // 処理
    } else {
        wg.Done()
        return
    }
}()

ケース3: パニックリカバリが必要な場合
WaitGroup.Go()のドキュメントには「関数fはパニックしてはいけない」と記載されています

// パニックリカバリが必要な場合は独自に実装
wg.Go(func() {
    defer func() {
        if r := recover(); r != nil {
            log.Printf("Recovered from panic: %v", r)
        }
    }()
    // 処理
})

まとめ

重要ポイント

  1. Go 1.25のWaitGroup.Go()は並行処理を簡潔に書ける

    • wg.Add(1)defer wg.Done()が不要
    • ボイラープレートコードを削減
  2. 関数シグネチャに注意

    • WaitGroup.Go()func()型の関数のみ受け取る
    • 引数を渡すパターンは使えない
  3. Go 1.22のループ変数変更が安全性を保証

    • 各イテレーションで新しい変数が作られる
    • クロージャで直接ループ変数を参照できる
  4. 移行は段階的に

    • Go 1.22以上であることを確認
    • テストでデータ競合をチェック(go test -race
    • パニックリカバリが必要な場合は独自実装

参考リンク

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?