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.Mutex か sync.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 付きでテストを実行する習慣をつけることで、並行処理のバグを未然に防げます。