導入
Go言語の強力な武器であるgoroutine。しかし、複数のgoroutineが同一のメモリ領域に対して同期なしで読み書きを行うと「データ競合(Data Race)」が発生し、予測不能なバグやクラッシュを招きます。これらはテスト時に顕在化しないことも多く、本番環境で発生するとデバッグが極めて困難です。本記事では、Goの標準機能である「-race」オプションを使い、開発段階でこれらの問題を確実に検知・排除する方法を解説します。
基礎知識
データ競合とは、複数のgoroutineが同じ変数に同時にアクセスし、少なくとも一つが書き込みを行っている状態を指します。Goのランタイムには「競合検知器(Race Detector)」が組み込まれており、プログラム実行時のメモリ操作を監視します。これは単なる静的解析ではなく、実行時の動的な監視であるため、テストコードの実行と組み合わせることで高い精度で問題を特定できます。
実装/解決策
競合検知を利用するには、Goのテストコマンドに -race フラグを付与するだけです。CI/CDパイプラインに組み込む際は、必ずこのフラグを有効にすることをお勧めします。競合が発生した場合、コンソール上に「WARNING: DATA RACE」という警告と共に、どのgoroutineが、どの箇所で競合を起こしているのかという詳細なスタックトレースが出力されます。
サンプルプログラム
以下のコードは、意図的にデータ競合を起こす例です。このコードに対して go test -race を実行すると、競合が検知されます。
// main_test.go
package main
import (
“sync”
“testing”
)
func TestDataRace(t testing.T) {
// 競合する共有変数
counter := 0
var wg sync.WaitGroup
// 複数のgoroutineで同時にインクリメントを行う
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ここでcounterへの読み書きが同時に発生し、競合する
counter++
}()
}
wg.Wait()
}
// 修正案:sync.Mutexを使用してアクセスを保護する
func TestFixedRace(t testing.T) {
counter := 0
var mu sync.Mutex
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ロックをかけることで競合を防ぐ
mu.Lock()
counter++
mu.Unlock()
}()
}
wg.Wait()
}
応用・注意点
注意点1:パフォーマンスへの影響
競合検知器は実行時にメモリアクセスを追跡するため、通常の実行よりもメモリ使用量が増加し、処理速度も低下します。そのため、本番環境のバイナリに組み込むべきではありません。あくまで「テスト時」や「開発用ビルド」に限定して使用してください。
注意点2:完全ではない
-raceは「テストが実行されたコードパス」に対してのみ競合を検知します。網羅的なテストが書かれていない場合、競合を見逃す可能性があるため、テストカバレッジを高める努力と併用することが重要です。
現場での活用法
CI(GitHub Actions等)のテストステップに「go test -race ./…」を追加しましょう。これにより、チーム開発において競合を含んだコードがメインブランチにマージされることを物理的に防ぐことができます。

コメント