Go の 'WARNING: DATA RACE' を修正する — データ競合の検出と対処法

intermediate🔷 Go2026-03-18| Go 1.16+、Linux / macOS / Windows — Goレースディテクターが動作する全プラットフォーム

Error Message

WARNING: DATA RACE
#go#race#concurrency#goroutine

エラーの内容

レースディテクターを有効にしてGoプログラムを実行すると、次のようなエラーが表示されます:

==================
WARNING: DATA RACE
Write at 0x00c0000b4010 by goroutine 7:
  main.incrementCounter()
      /home/user/app/main.go:15 +0x30

Previous read at 0x00c0000b4010 by goroutine 6:
  main.readCounter()
      /home/user/app/main.go:22 +0x2e

Goroutine 7 (running) created at:
  main.main()
      /home/user/app/main.go:35 +0x6e
==================
Found 1 data race(s)
exit status 66

または、go test -race ./... 実行中にレースディテクターが発動してCIパイプラインが失敗することもあります。いずれにせよ、これは深刻な問題です — 本番環境でのサイレントなデータ破損はデバッグが難しく、再現も困難です。

発生原因

データ競合は、2つのgoroutineが同じメモリ領域に同時アクセスし、少なくとも一方が書き込みであり、かつgoroutine間に同期処理がない場合に発生します。GoのランタイムレースディテクターはThreadSanitizerをベースに構築されており、実行時にメモリアクセスを計測してこれを検出します。

よくある原因:

  • mutexなしで複数のgoroutineからインクリメントされるグローバルカウンター
  • 並行して書き込みと読み込みが行われる map — mapはgoroutineセーフではありません
  • 複数のgoroutineから同時に要素が追加されるスライス
  • あるgoroutineがフィールドを更新している間に別のgoroutineが読み込む構造体フィールド

ステップ 1 — レースディテクターを有効にする

まだ -race フラグを使っていない場合は有効にしましょう。出力には、どのファイルの何行目で、どのgoroutineが競合を引き起こしたかが正確に示されます:

# レース検出を有効にしてバイナリを実行
go run -race main.go

# レース検出を有効にしてテストを実行
go test -race ./...

# レース計測済みバイナリをビルド
go build -race -o myapp .

スタックトレースをよく読んでください。どのgoroutineが書き込み、どのgoroutineが読み込んだか、そしてそれぞれがどこで生成されたかが分かります。まずそこから始めましょう。

ステップ 2 — 競合を修正する

状況に合ったアプローチを選んでください。

オプション A: sync.Mutex(最も一般的な修正方法)

mutexで共有変数を保護します。1つのgoroutineがロックを保持し、他のgoroutineは待機します。

修正前(バグあり):

var counter int

func increment() {
    counter++ // DATA RACE: 複数のgoroutineがここにアクセスする
}

func main() {
    for i := 0; i < 1000; i++ {
        go increment()
    }
}

修正後(Mutexを使用):

import "sync"

var (
    counter int
    mu      sync.Mutex
)

func increment() {
    mu.Lock()
    counter++
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }
    wg.Wait()
    fmt.Println(counter) // 常に1000
}

オプション B: sync.RWMutex(読み込みが多いワークロード向け)

読み込みが多く書き込みが少ない場合は、RWMutex を使いましょう。複数のgoroutineが同時に読み込み可能で、書き込み時のみ全goroutineをブロックします:

var (
    data map[string]string
    mu   sync.RWMutex
)

func read(key string) string {
    mu.RLock()
    defer mu.RUnlock()
    return data[key]
}

func write(key, value string) {
    mu.Lock()
    defer mu.Unlock()
    data[key] = value
}

オプション C: sync/atomic(シンプルなカウンターとフラグ向け)

整数カウンターやブールフラグにはフルのmutexは不要です。sync/atomic はより高速で、ロック競合を完全に回避できます:

import "sync/atomic"

var counter int64

func increment() {
    atomic.AddInt64(&counter, 1)
}

func getCount() int64 {
    return atomic.LoadInt64(&counter)
}

オプション D: チャネル(共有ではなく通信で解決)

チャネルはメモリを共有せずにgoroutine間でデータの所有権を移転します。共有状態がなければ競合も発生しません:

func worker(jobs <-chan int, results chan<- int) {
    for j := range jobs {
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    go worker(jobs, results)

    jobs <- 5
    close(jobs)
    fmt.Println(<-results) // 10
}

オプション E: sync.Map(並行mapアクセス向け)

map で競合が発生している場合は、sync.Map に置き換えましょう。追加のロック処理は不要です:

var m sync.Map

// 書き込み
m.Store("key", "value")

// 読み込み
val, ok := m.Load("key")
if ok {
    fmt.Println(val.(string))
}

ステップ 3 — 修正を確認する

レースディテクターを有効にして再実行します。競合がゼロであれば完了です:

go test -race ./...
# 期待される出力(競合なし):
ok      github.com/yourorg/app    0.512s

go run -race main.go
# WARNING: DATA RACE の出力なしで実行される

さらに確信を持ちたい場合は、並行数を増やして複数回実行してみましょう:

# 並行数を最大にしてテストを実行
go test -race -count=10 -parallel=8 ./...

10回の並行実行でクリーンであれば、修正は完了です。

実践的なヒント

  • CIでは常に -race を実行しましょう。 ディテクターはCPUオーバーヘッドが2〜20倍、メモリオーバーヘッドが5〜10倍になります — 本番環境には重すぎますが、テスト実行には十分許容範囲です。
  • sync.Mutex を値コピーしてはいけません。 mutexを値渡しすると動作が壊れますが、エラーは出ません。常にポインターを使用するか、ポインターで渡される構造体に埋め込んでください。
  • チャネルはデータ転送に、mutexは共有状態の管理に使います。 あらゆる操作にmutexを使おうとするのは設計の問題です。goroutineの構造を見直しましょう。
  • goroutine内のクロージャはよくある落とし穴です。 クロージャでキャプチャされたループ変数は競合の典型的な原因です。代わりに変数を引数として渡しましょう:
// バグ: 全goroutineが同じ `i` をキャプチャする
for i := 0; i < 5; i++ {
    go func() { fmt.Println(i) }()
}

// 修正済み: `i` を引数として渡す
for i := 0; i < 5; i++ {
    go func(i int) { fmt.Println(i) }(i)
}

Related Error Notes