Go の race detector でデータ競合を検出する

並行処理ではデータ競合(race condition)が厄介なバグの原因になります。Go には -race フラグで実行時にデータ競合を検出する機能が組み込まれています。これを「race detector」と呼びます。

データ競合とは

複数の goroutine が同じ変数に同時にアクセスし、少なくとも 1 つが書き込みを行う場合、データ競合が発生します。

package main

import (
    "fmt"
    "sync"
)

func main() {
    counter := 0
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // データ競合!
        }()
    }

    wg.Wait()
    fmt.Println(counter) // 結果は不定
}

このコードは一見動きますが、counter++ は「読み取り → 加算 → 書き込み」の 3 ステップで構成されており、複数の goroutine が同時に実行すると値が失われます。

race detector の使い方

-race フラグを付けてビルドまたは実行すると、race detector が有効になります。

# 実行時に検出
go run -race main.go

# テスト時に検出
go test -race ./...

# ビルド時に組み込み
go build -race -o myapp

実行すると以下のような警告が出力されます。

WARNING: DATA RACE
Read at 0x00c0000b4010 by goroutine 7:
  main.main.func1()
      /path/to/main.go:15 +0x4e

Previous write at 0x00c0000b4010 by goroutine 6:
  main.main.func1()
      /path/to/main.go:15 +0x60

Goroutine 7 (running) created at:
  main.main()
      /path/to/main.go:13 +0x7e

どの行で、どの goroutine が競合しているかが詳細に表示されます。

検出例:マップへの同時アクセス

Go のマップは並行アクセスに対応していません。

func main() {
    m := make(map[int]int)
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m[n] = n * 2 // データ競合!
        }(i)
    }

    wg.Wait()
}

go run -race main.go で実行すると、マップへの同時書き込みが検出されます。これは sync.Mutexsync.Map で保護する必要があります。

修正例

先ほどのカウンタを正しく修正するには、いくつかの方法があります。

// 方法 1: Mutex を使う
func withMutex() {
    var mu sync.Mutex
    counter := 0
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            counter++
            mu.Unlock()
        }()
    }

    wg.Wait()
    fmt.Println(counter) // 必ず 1000
}

// 方法 2: atomic を使う
func withAtomic() {
    var counter int64
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1)
        }()
    }

    wg.Wait()
    fmt.Println(counter) // 必ず 1000
}

修正後のコードを -race で実行しても、警告は出なくなります。

CI への組み込み

race detector はテストに組み込むのがおすすめです。

# CI の設定例(GitHub Actions)
- name: Test with race detector
  run: go test -race -v ./...

すべてのテストを -race 付きで実行することで、並行処理のバグを早期に発見できます。ただし、race detector はメモリと CPU を追加で消費するため、本番ビルドには含めないのが一般的です。

検出できないケース

race detector は万能ではありません。

実行されないパスは検出不可

テストでカバーされていないコードパスの競合は検出できません。

論理的な競合は検出不可

デッドロックや、正しく同期されているが論理的に間違っているコードは検出対象外です。

race detector は「物理的なデータ競合」を検出するツールです。並行処理の正しさを保証するものではないため、設計段階での考慮も重要です。

本番環境での注意

race detector を有効にしたバイナリは、メモリ使用量が約 5〜10 倍、実行速度が約 2〜20 倍遅くなります。開発・テスト環境でのみ使用し、本番環境では無効にしてください。

# 開発時
go run -race main.go

# 本番ビルド(-race なし)
go build -o myapp main.go

定期的に -race 付きでテストを実行する習慣をつけることで、並行処理のバグを未然に防げます。