【Go言語学習|豆知識】Goのテストで隠れた競合を見つける!`go test -cpu`オプションの活用術

導入

Go言語は並行処理が非常に強力ですが、その分「データ競合(Race Condition)」という厄介なバグが潜みやすいという側面があります。開発環境でテストが通っていても、本番環境のマルチコアCPU上では不可解な挙動を示すことは珍しくありません。そこで活用したいのが `go test -cpu=N` オプションです。この機能を使うことで、特定の並列度でのテストを強制し、マルチスレッド環境特有のバグを早期に発見できます。

基礎知識

Goのランタイムにおいて、`GOMAXPROCS` は同時に実行可能な最大CPUコア数を制御する環境変数です。通常、Goは利用可能なすべてのコアを使用しますが、テスト時に `-cpu` フラグを指定すると、この `GOMAXPROCS` を動的に切り替えてテストを実行できます。これにより、シングルコア環境での動作と、マルチコア環境での並列動作の両方を一つのコマンドで検証することが可能になります。

実装/解決策

具体的な運用方法として、複数のコア数をカンマ区切りで指定します。例えば `go test -cpu=1,2,4` と実行すると、まずシングルコア(GOMAXPROCS=1)で全テストを実行し、次に2コア、最後に4コアという順序で繰り返しテストが行われます。もし、ある並列度でのみテストが失敗する場合、そのコードには排他制御の漏れや、共有変数への不正なアクセスがある可能性が高いと判断できます。

サンプルプログラム

以下のコードは、共有カウンタをインクリメントする際、並列度によって挙動が変化する可能性のある例です。

package main

import (
“sync”
“testing”
)

// 共有のリソースを模した構造体
type Counter struct {
mu sync.Mutex
value int
}

func (c Counter) Increment() {
// 意図的にロックを外すと、-cpu=2,4 でテストが失敗する可能性が高まります
c.mu.Lock()
defer c.mu.Unlock()
c.value++
}

func TestCounter(t testing.T) {
c := &Counter{}
var wg sync.WaitGroup

// 1000回並行でインクリメントを行う
for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() c.Increment() }() } wg.Wait() // 期待値通りか確認 if c.value != 1000 { t.Errorf("期待値は 1000 ですが、%d でした", c.value) } }

応用・注意点

このオプションを使う際の注意点は、「テストが遅くなる可能性がある」という点です。コア数を増やすとテスト実行回数も増えるため、CIパイプラインに組み込む際は実行時間とのバランスを考慮してください。また、`go test -race` オプションと併用することで、競合の検出精度を大幅に高めることができます。現場では `go test -race -cpu=1,4 ./…` のように組み合わせて運用するのが、堅牢なバックエンド開発の定石です。ぜひ次の開発から、単なる `go test` ではなく、マルチコア環境を意識したテストを取り入れてみてください。

コメント

タイトルとURLをコピーしました