第16章: テスト - 機械レベルの深層理解
学習目標
この章を終えると、以下ができるようになります:
- テストバイナリのビルドプロセスを理解できる
- testing.Tの内部構造とメソッドの動作原理を説明できる
- カバレッジ計測のソースコード挿入メカニズムを理解できる
- ベンチマークにおけるb.N決定アルゴリズムを理解できる
- Race Detector(ThreadSanitizer)の動作原理を理解できる
🔑 テストバイナリのビルドプロセス
go testの内部フロー
go testコマンドは、通常のビルドとは異なる特別なビルドプロセスを実行します。
go test 実行フロー:
Phase 1: テストファイル検出
┌────────────────────────────────┐
│ 1. パッケージディレクトリ走査 │
│ *.go ファイル検索 │
│ *_test.go ファイル検索 │
│ │
│ 2. ファイル分類 │
│ - プロダクションコード │
│ - テストコード │
│ - 外部テスト (_test package)│
└────────┬───────────────────────┘
Phase 2: テストレジストリ生成
┌────────────────────────────────┐
│ 3. Test関数の抽出 │
│ func TestXxx(*testing.T) │
│ │
│ 4. Benchmark関数の抽出 │
│ func BenchmarkXxx(*testing.B)│
│ │
│ 5. Example関数の抽出 │
│ func ExampleXxx() │
│ │
│ 6. _testmain.go 生成 │
│ 自動生成されたmain関数 │
└────────┬───────────────────────┘
Phase 3: コンパイル
┌────────────────────────────────┐
│ 7. テストバイナリのコンパイル │
│ - プロダクションコード │
│ - テストコード │
│ - _testmain.go │
│ │
│ 8. 実行可能ファイル生成 │
│ package.test (バイナリ) │
└────────┬───────────────────────┘
Phase 4: 実行
┌────────────────────────────────┐
│ 9. テストバイナリ実行 │
│ ./package.test │
│ │
│ 10. 結果収集と出力 │
│ PASS/FAIL判定 │
└────────────────────────────────┘
🔑 _testmain.goの生成
go testは自動的に_testmain.goというファイルを生成します。このファイルがテスト実行のエントリポイントになります。
// _testmain.go の簡略版(自動生成)
package main
import (
"os"
"testing"
"testing/internal/testdeps"
_test "mypackage" // テスト対象パッケージ
)
var tests = []testing.InternalTest{
{"TestAdd", _test.TestAdd},
{"TestSubtract", _test.TestSubtract},
// ... 全てのTest関数
}
var benchmarks = []testing.InternalBenchmark{
{"BenchmarkAdd", _test.BenchmarkAdd},
// ... 全てのBenchmark関数
}
var examples = []testing.InternalExample{
{"ExampleAdd", _test.ExampleAdd, "", false},
// ... 全てのExample関数
}
func main() {
m := testing.MainStart(
testdeps.TestDeps{},
tests,
benchmarks,
examples,
)
os.Exit(m.Run())
}
生成プロセス:
_testmain.go生成アルゴリズム:
1. ソースファイルのパース
┌──────────────────────────┐
│ for each *_test.go: │
│ AST生成 │
│ 関数宣言を抽出 │
└──────┬───────────────────┘
2. テスト関数の識別
┌──────────────────────────┐
│ 関数名がTest始まり? │
│ 第一引数が*testing.T? │
│ → testsリストに追加 │
└──────┬───────────────────┘
3. ベンチマーク関数の識別
┌──────────────────────────┐
│ 関数名がBenchmark始まり?│
│ 第一引数が*testing.B? │
│ → benchmarksリストに追加 │
└──────┬───────────────────┘
4. テンプレート展開
┌──────────────────────────┐
│ template.Execute() │
│ testsスライス生成 │
│ benchmarksスライス生成 │
│ main()関数生成 │
└──────────────────────────┘
疑似コード:
type TestFunc struct {
Name string
Func func(*testing.T)
}
func generateTestMain(pkg *Package) string {
tests := []TestFunc{}
benchmarks := []BenchFunc{}
// ASTから関数を抽出
for _, file := range pkg.TestFiles {
ast := parseFile(file)
for _, decl := range ast.Decls {
if fn, ok := decl.(*FuncDecl); ok {
if strings.HasPrefix(fn.Name, "Test") {
tests = append(tests, TestFunc{
Name: fn.Name,
Func: fn,
})
} else if strings.HasPrefix(fn.Name, "Benchmark") {
benchmarks = append(benchmarks, ...)
}
}
}
}
// テンプレート生成
return executeTemplate(testMainTemplate, struct{
Tests []TestFunc
Benchmarks []BenchFunc
}{tests, benchmarks})
}
テストバイナリのメモリレイアウト
テストバイナリの構成:
┌────────────────────────────────┐
│ Text Segment (.text) │
│ ───────────────────────────── │
│ プロダクションコード │
│ - Add(), Subtract()等 │
│ │
│ テストコード │
│ - TestAdd(), TestSubtract()等 │
│ │
│ testing パッケージコード │
│ - (*T).Run(), (*T).Error()等 │
│ │
│ _testmain.go のコード │
│ - main()関数 │
└────────────────────────────────┘
┌────────────────────────────────┐
│ Data Segment (.data/.bss) │
│ ───────────────────────────── │
│ tests スライス │
│ benchmarks スライス │
│ グローバル変数 │
└────────────────────────────────┘
┌────────────────────────────────┐
│ Read-Only Data (.rodata) │
│ ───────────────────────────── │
│ テスト名文字列 │
│ "TestAdd", "TestSubtract" │
└────────────────────────────────┘
🔑 testing.Tの内部構造
testing.T構造体の詳細
// src/testing/testing.go の簡略版
type T struct {
common // 共通フィールド
isParallel bool // t.Parallel()が呼ばれたか
context *testContext
}
type common struct {
mu sync.RWMutex // ロック(並列テスト用)
output []byte // ログ出力バッファ
w io.Writer // 出力先
name string // テスト名 "TestAdd/subtest"
failed bool // 失敗フラグ
skipped bool // スキップフラグ
finished bool // 完了フラグ
hasSub atomic.Bool // サブテストがあるか
raceErrors int // レースエラー数
runner string // 実行中のゴルーチン名
parent *common // 親テスト(サブテストの場合)
level int // ネストレベル
creator []uintptr // 作成元スタックトレース
start time.Time // 開始時刻
duration time.Duration // 実行時間
barrier chan bool // 同期用チャネル
signal chan bool // シグナル用チャネル
sub []*T // サブテストのリスト
}
メモリレイアウト (64bit):
T構造体: 約200 bytes
┌────────────────────────────────┐
│ common (約180 bytes) │
│ ├─ mu (32 bytes) │
│ ├─ output (24 bytes: スライス) │
│ ├─ w (16 bytes: interface) │
│ ├─ name (16 bytes: string) │
│ ├─ failed (1 byte) │
│ ├─ skipped (1 byte) │
│ ├─ finished (1 byte) │
│ ├─ parent (8 bytes: pointer) │
│ ├─ start (24 bytes: Time) │
│ ├─ duration (8 bytes) │
│ └─ ... │
│ │
│ isParallel (1 byte) │
│ context (8 bytes: pointer) │
└────────────────────────────────┘
🔑 testing.T.Run()の動作
サブテストの実行メカニズムを詳しく見ていきます。
// t.Run()の簡略版実装
func (t *T) Run(name string, f func(*testing.T)) bool {
// 1. サブテスト構造体作成
t.hasSub.Store(true)
sub := &T{
common: common{
parent: &t.common,
level: t.level + 1,
name: t.name + "/" + name,
w: t.w,
barrier: make(chan bool),
signal: make(chan bool),
},
context: t.context,
}
// 2. テストレジストリに登録
t.mu.Lock()
t.sub = append(t.sub, sub)
t.mu.Unlock()
// 3. 並列実行判定
if t.isParallel {
// 並列実行:新しいゴルーチンで実行
go func() {
sub.run(f)
}()
} else {
// 逐次実行:現在のゴルーチンで実行
sub.run(f)
}
// 4. 結果を返す
return !sub.failed
}
func (t *T) run(f func(*testing.T)) {
defer func() {
// パニック時の処理
if r := recover(); r != nil {
t.Errorf("panic: %v\n%s", r, debug.Stack())
}
t.finished = true
}()
// 開始時刻記録
t.start = time.Now()
// テスト関数実行
f(t)
// 実行時間記録
t.duration = time.Since(t.start)
}
実行トレース例:
TestParent の実行:
Step 1: TestParent開始
T{name: "TestParent", level: 0}
Step 2: t.Run("sub1", ...)呼び出し
新しいT作成
T{name: "TestParent/sub1", level: 1, parent: &TestParent}
Step 3: sub1実行
sub1.run(f) → f(&sub1)
Step 4: t.Run("sub2", ...)呼び出し
新しいT作成
T{name: "TestParent/sub2", level: 1, parent: &TestParent}
Step 5: sub2実行
sub2.run(f) → f(&sub2)
Step 6: TestParent完了
t.duration = time.Since(t.start)
テストツリー構造:
TestParent (0.5s)
├─ sub1 (0.2s)
└─ sub2 (0.3s)
🔑 testing.T.Errorf()のバッファリング
// t.Errorf()の内部実装
func (t *T) Errorf(format string, args ...interface{}) {
t.Error(fmt.Sprintf(format, args...))
}
func (t *T) Error(args ...interface{}) {
t.Fail() // failedフラグをセット
t.log(args...)
}
func (t *T) log(args ...interface{}) {
t.mu.Lock()
defer t.mu.Unlock()
// ログメッセージをバッファに追加
msg := fmt.Sprintln(args...)
t.output = append(t.output, []byte(msg)...)
// -vフラグがある場合は即座に出力
if *verbose {
t.flushToWriter()
}
}
func (t *T) flushToWriter() {
// バッファの内容を出力
if len(t.output) > 0 {
// インデント追加
indent := strings.Repeat(" ", t.level)
lines := bytes.Split(t.output, []byte("\n"))
for _, line := range lines {
if len(line) > 0 {
fmt.Fprintf(t.w, "%s%s\n", indent, line)
}
}
t.output = t.output[:0] // バッファクリア
}
}
出力バッファの構造:
並列テストなし:
┌────────────────────────────────┐
│ Test1: output buffer │
│ → 即座に出力 │
│ │
│ Test2: output buffer │
│ → 即座に出力 │
└────────────────────────────────┘
並列テストあり:
┌────────────────────────────────┐
│ Test1: output buffer (goroutine 1)
│ Test2: output buffer (goroutine 2)
│ Test3: output buffer (goroutine 3)
│ │
│ 完了後にmu.Lock()で順次出力 │
└────────────────────────────────┘
🔑 カバレッジ計測の仕組み
ソースコード挿入メカニズム
go test -coverを実行すると、コンパイラはソースコードにカバレッジ計測用のコードを自動挿入します。
カバレッジ計測フロー:
Phase 1: ソースコード解析
┌────────────────────────────────┐
│ 1. ASTパース │
│ 元のソースコード読み込み │
│ │
│ 2. 基本ブロック識別 │
│ 実行パスの分岐点を特定 │
└────────┬───────────────────────┘
Phase 2: カウンタ挿入
┌────────────────────────────────┐
│ 3. カウンタ配列生成 │
│ var GoCover = struct { │
│ Count [N]uint32 │
│ Pos [N]Pos │
│ NumStmt [N]uint16 │
│ } │
│ │
│ 4. 各基本ブロックに挿入 │
│ GoCover.Count[i]++ │
└────────┬───────────────────────┘
Phase 3: コンパイルと実行
┌────────────────────────────────┐
│ 5. 改変されたコードをコンパイル│
│ │
│ 6. テスト実行 │
│ カウンタがインクリメント │
│ │
│ 7. カバレッジ計算 │
│ 実行ブロック / 全ブロック │
└────────────────────────────────┘
🔑 具体的なコード変換例
// 元のコード:
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// -coverフラグでコンパイル後(簡略版):
var GoCover_0 = struct {
Count [4]uint32
Pos [4]string
NumStmt [4]uint16
}{
Pos: [4]string{
"file.go:10.37,11.14", // ブロック0: 関数開始〜if文
"11.14,13.3", // ブロック1: ifブロック内
"13.3,15.2", // ブロック2: return a/b
"", // 未使用
},
NumStmt: [4]uint16{1, 2, 2, 0},
}
func Divide(a, b int) (int, error) {
GoCover_0.Count[0]++ // ← 挿入されたカウンタ
if b == 0 {
GoCover_0.Count[1]++ // ← 挿入されたカウンタ
return 0, errors.New("division by zero")
}
GoCover_0.Count[2]++ // ← 挿入されたカウンタ
return a / b, nil
}
基本ブロックの識別アルゴリズム:
基本ブロック = 分岐なしで実行される最大の文の列
識別ルール:
1. 関数の開始点
2. 分岐先(if, for, switch等の直後)
3. 分岐元の次の文
4. return文の前
例:
func example(x int) {
// ブロック0: 開始
a := x + 1
if a > 10 { // ブロック0終了
// ブロック1: if真の分岐
fmt.Println("big")
return
}
// ブロック2: if後の継続
fmt.Println("small")
}
ブロック分割:
Block 0: a := x + 1; if a > 10 {...}
Block 1: fmt.Println("big"); return
Block 2: fmt.Println("small")
制御フローグラフ:
[0]
/ \
[1] [2]
カバレッジ計算
// カバレッジ計算の疑似コード
type CoverageProfile struct {
Blocks []CoverageBlock
}
type CoverageBlock struct {
StartLine int
StartCol int
EndLine int
EndCol int
NumStmt int
Count int // 実行回数
}
func CalculateCoverage(profile CoverageProfile) float64 {
totalStmts := 0
coveredStmts := 0
for _, block := range profile.Blocks {
totalStmts += block.NumStmt
if block.Count > 0 {
coveredStmts += block.NumStmt
}
}
if totalStmts == 0 {
return 0.0
}
return float64(coveredStmts) / float64(totalStmts) * 100.0
}
実行例:
Block 0: NumStmt=2, Count=10 → カバー済み (2文)
Block 1: NumStmt=2, Count=0 → 未カバー
Block 2: NumStmt=1, Count=10 → カバー済み (1文)
計算:
totalStmts = 2 + 2 + 1 = 5
coveredStmts = 2 + 0 + 1 = 3
coverage = 3 / 5 * 100 = 60.0%
🔑 カバレッジプロファイルのフォーマット
coverprofile形式:
mode: set
mypackage/math.go:10.37,11.14 1 1
mypackage/math.go:11.14,13.3 2 0
mypackage/math.go:13.3,15.2 2 1
形式:
<file>:<startLine>.<startCol>,<endLine>.<endCol> <numStmt> <count>
解説:
mode: カウントモード
- set: 実行されたか (0 or 1)
- count: 実行回数
- atomic: アトミックカウンタ(並列テスト用)
<count>:
0 = 未実行
1+ = 実行された(回数)
🔑 ベンチマークと b.N の決定
testing.B構造体
// src/testing/benchmark.go の簡略版
type B struct {
common
importPath string
context *benchContext
N int // イテレーション回数 ←重要
previousN int
previousDuration time.Duration
benchFunc func(*B)
benchTime benchTimeFlag // -benchtime フラグ
bytes int64
missingBytes bool
timerOn bool
showAllocResult bool
result BenchmarkResult
parallelism int // -cpu フラグ
// StartTimer/StopTimer用
start time.Time
duration time.Duration
netAllocs uint64
netBytes uint64
}
type BenchmarkResult struct {
N int // イテレーション数
T time.Duration // 総実行時間
Bytes int64 // 処理バイト数
MemAllocs uint64 // アロケーション回数
MemBytes uint64 // アロケーションバイト数
}
🔑 b.N決定アルゴリズム
ベンチマークは自動的に適切なイテレーション回数を決定します。
b.N決定アルゴリズム:
目標: 1秒間実行されるNを見つける
Phase 1: 初期推定
┌────────────────────────────────┐
│ N = 1 で実行 │
│ 実行時間を測定 │
│ │
│ 速すぎる場合(< 100ms): │
│ N を増やして再試行 │
└────────┬───────────────────────┘
Phase 2: 反復調整
┌────────────────────────────────┐
│ while 実行時間 < 目標時間: │
│ 前回の実行時間から推定 │
│ N = N * (目標時間 / 実際時間)│
│ 再実行 │
└────────┬───────────────────────┘
Phase 3: 最終測定
┌────────────────────────────────┐
│ 決定されたNで実行 │
│ 結果を記録 │
└────────────────────────────────┘
疑似コード:
func (b *B) run() {
const (
targetTime = 1 * time.Second
minTime = 100 * time.Millisecond
)
// Phase 1: 初期N決定
b.N = 1
for {
b.runN(b.N)
if b.duration >= minTime {
break
}
// Nを増やす(10倍、100倍...)
if b.duration < minTime/10 {
b.N *= 100
} else {
b.N *= 10
}
}
// Phase 2: 目標時間に向けて調整
for b.duration < targetTime {
prevN := b.N
prevDuration := b.duration
// 線形外挿で次のNを推定
// N_new = N_old * (target / actual)
ratio := float64(targetTime) / float64(prevDuration)
b.N = int(float64(prevN) * ratio)
// 最低でも1増やす
if b.N <= prevN {
b.N = prevN + 1
}
b.runN(b.N)
// 収束判定
if b.duration >= targetTime {
break
}
}
// Phase 3: 最終測定
b.result = BenchmarkResult{
N: b.N,
T: b.duration,
Bytes: b.bytes,
// ...
}
}
func (b *B) runN(n int) {
b.N = n
b.ResetTimer()
b.start = time.Now()
b.benchFunc(b) // ベンチマーク関数実行
b.duration = time.Since(b.start)
}
実行トレース例:
ベンチマーク: func BenchmarkAdd(b *testing.B) { ... }
Iteration 1:
N = 1
duration = 50ns
→ 速すぎる (< 100ms)
→ N *= 100
Iteration 2:
N = 100
duration = 5μs
→ まだ速い
→ N *= 100
Iteration 3:
N = 10,000
duration = 500μs
→ まだ速い
→ N *= 100
Iteration 4:
N = 1,000,000
duration = 50ms
→ 目標に近い
→ ratio = 1s / 50ms = 20
→ N = 1,000,000 * 20 = 20,000,000
Iteration 5:
N = 20,000,000
duration = 1.02s
→ 目標達成!
最終結果:
BenchmarkAdd-8 20000000 51.2 ns/op
^^^^^^^^ ^^^^^^^^
N T/N
🔑 ns/opの計算
// ナノ秒/オペレーションの計算
func (r BenchmarkResult) NsPerOp() int64 {
if r.N <= 0 {
return 0
}
return r.T.Nanoseconds() / int64(r.N)
}
例:
N = 20,000,000
T = 1.024秒 = 1,024,000,000 ナノ秒
ns/op = 1,024,000,000 / 20,000,000
= 51.2 ns/op
メモリアロケーション表示:
BenchmarkAdd-8 20000000 51.2 ns/op 32 B/op 1 allocs/op
^^^^^^^ ^^^^^^^^^^^
MemBytes MemAllocs
/N /N
計算:
B/op = MemBytes / N
allocs/op = MemAllocs / N
b.ResetTimer()の動作
// タイマーリセットの実装
func (b *B) ResetTimer() {
if b.timerOn {
// 既存の時間を減算
// セットアップ時間を除外するため
b.start = time.Now()
b.duration = 0
b.netAllocs = 0
b.netBytes = 0
}
}
使用例:
func BenchmarkWithSetup(b *testing.B) {
// セットアップ(測定対象外)
data := make([]int, 1000000)
for i := range data {
data[i] = rand.Int()
}
b.ResetTimer() // ← ここからタイマー開始
for i := 0; i < b.N; i++ {
sort.Ints(data)
}
}
タイムライン:
|<-- Setup -->|<-- Timer Reset -->|<-- Measured -->|
0ms 100ms 100ms 1100ms
^^^^^^^^^^^^^^^^
この部分のみ測定
🔑 Race Detector(ThreadSanitizer)
Race Detectorの動作原理
Goのrace detectorは、Googleが開発したThreadSanitizer(TSan)をベースにしています。
Race Detector動作フロー:
Phase 1: インストルメンテーション(コード挿入)
┌────────────────────────────────────┐
│ コンパイル時: │
│ 全てのメモリアクセスに対して │
│ トラッキングコードを挿入 │
│ │
│ 元のコード: │
│ x = 42 │
│ │
│ 挿入後: │
│ __tsan_write(addr, size, pc) │
│ x = 42 │
└────────┬───────────────────────────┘
Phase 2: 実行時トラッキング
┌────────────────────────────────────┐
│ 各ゴルーチンごとに: │
│ - Vector Clock管理 │
│ - Shadow Memory維持 │
│ │
│ メモリアクセスごとに: │
│ - 前回のアクセス情報取得 │
│ - Happens-Before関係チェック │
│ - レース検出 │
└────────┬───────────────────────────┘
Phase 3: レース報告
┌────────────────────────────────────┐
│ レース検出時: │
│ - 両方のアクセス位置特定 │
│ - スタックトレース記録 │
│ - レポート生成 │
└────────────────────────────────────┘
🔑 Vector Clock アルゴリズム
Vector Clockは、分散システムにおける因果関係を追跡するアルゴリズムです。
Vector Clock の概念:
各ゴルーチンGiは、ベクトル時計VC[i]を持つ
VC[i] = [c0, c1, c2, ..., cn]
^ ^ ^ ^
G0 G1 G2 Gn のカウント
操作:
1. ローカルイベント:
VC[i][i]++
2. メッセージ送信(チャネル送信等):
VC[i][i]++
メッセージにVC[i]を添付
3. メッセージ受信(チャネル受信等):
VC[i] = max(VC[i], 受信したVC)
VC[i][i]++
Happens-Before関係:
VC1 < VC2 iff ∀i: VC1[i] <= VC2[i] かつ ∃j: VC1[j] < VC2[j]
並行(レース):
VC1と VC2が並行 iff VC1 < VC2 でも VC2 < VC1 でもない
具体例:
2つのゴルーチンによるレース:
時刻 G1の動作 VC[1] G2の動作 VC[2]
────────────────────────────────────────────────────────
t0 起動 [1,0] 起動 [0,1]
t1 x = 1 [2,0]
(write)
t2 y = x [0,2]
(read)
Vector Clock比較:
Write時: VC_write = [2,0]
Read時: VC_read = [0,2]
判定:
[2,0] < [0,2] ? → No (2 > 0)
[0,2] < [2,0] ? → No (2 > 0)
→ 並行アクセス(レース)検出!
正常なケース(同期あり):
時刻 G1の動作 VC[1] G2の動作 VC[2]
────────────────────────────────────────────────────────
t0 起動 [1,0] 起動 [0,1]
t1 x = 1 [2,0]
(write)
t2 ch <- msg [3,0]
msg.VC=[3,0]
t3 msg := <-ch [3,2]
merge VCs
t4 y = x [3,3]
(read)
判定:
[3,0] < [3,3] ? → Yes
→ Happens-Before関係あり(レースなし)
🔑 Shadow Memory
Shadow Memoryは、各メモリアドレスの最後のアクセス情報を記録します。
Shadow Memory構造:
実メモリ Shadow Memory
──────── ──────────────────────────
0x1000 {lastWrite: VC[2,0], tid: 1}
0x1001 {lastWrite: VC[1,1], tid: 2}
0x1002 {lastRead: VC[3,2], tid: 1}
... ...
各エントリ:
struct ShadowCell {
LastAccess VectorClock
ThreadID int
IsWrite bool
PC uintptr // プログラムカウンタ(位置情報)
}
メモリマッピング:
実アドレス → Shadow アドレス の変換式:
shadow_addr = (real_addr >> 3) + SHADOW_OFFSET
理由: 8バイトごとに1つのShadowエントリ
メモリ効率のため圧縮
アクセス時のレースチェック:
func CheckWrite(addr uintptr, vc VectorClock, tid int) {
shadow := GetShadow(addr)
// 前回の読み取りとのレースチェック
for _, read := range shadow.LastReads {
if !HappensBefore(read.VC, vc) {
ReportRace(read, CurrentAccess{vc, tid, true})
}
}
// 前回の書き込みとのレースチェック
if shadow.LastWrite != nil {
if !HappensBefore(shadow.LastWrite.VC, vc) {
ReportRace(shadow.LastWrite, CurrentAccess{vc, tid, true})
}
}
// 今回のアクセスを記録
shadow.LastWrite = &Access{VC: vc, TID: tid, PC: GetPC()}
}
Race Detectorのコード挿入例
// 元のコード:
var counter int
func increment() {
counter++
}
// -raceフラグでコンパイル後(概念的):
var counter int
func increment() {
// 読み取り
__tsan_read(uintptr(&counter), 8, getcallerpc())
// 書き込み
__tsan_write(uintptr(&counter), 8, getcallerpc())
counter++
}
実際のランタイム関数:
// runtime/race.go
func raceread(addr unsafe.Pointer)
func racewrite(addr unsafe.Pointer)
func racereadrange(addr unsafe.Pointer, len int)
func racewriterange(addr unsafe.Pointer, len int)
同期プリミティブもインストルメント:
// sync.Mutex.Lock()
func (m *Mutex) Lock() {
// ... ロック獲得 ...
// レースディテクタに通知
raceAcquire(unsafe.Pointer(m))
}
// sync.Mutex.Unlock()
func (m *Mutex) Unlock() {
// レースディテクタに通知
raceRelease(unsafe.Pointer(m))
// ... ロック解放 ...
}
// チャネル送信
func chansend(...) {
// ... 送信処理 ...
racerelease(c) // Vector Clockを送信
}
// チャネル受信
func chanrecv(...) {
raceacquire(c) // Vector Clockをマージ
// ... 受信処理 ...
}
🔑 レースレポートの生成
レース検出時の出力例:
==================
WARNING: DATA RACE
Write at 0x00c000012088 by goroutine 7:
main.increment()
/path/to/main.go:15 +0x3e
main.worker()
/path/to/main.go:20 +0x3b
Previous write at 0x00c000012088 by goroutine 6:
main.increment()
/path/to/main.go:15 +0x3e
main.worker()
/path/to/main.go:20 +0x3b
Goroutine 7 (running) created at:
main.main()
/path/to/main.go:10 +0x7e
Goroutine 6 (finished) created at:
main.main()
/path/to/main.go:10 +0x7e
==================
レポート構造:
1. 現在のアクセス(Write/Read)
- メモリアドレス
- ゴルーチンID
- スタックトレース
2. 競合する過去のアクセス(Previous write/read)
- メモリアドレス
- ゴルーチンID
- スタックトレース
3. ゴルーチン作成位置
- 両方のゴルーチンの起動元
Race Detectorのオーバーヘッド
パフォーマンス影響:
メモリ使用量: 5-10倍増加
理由: Shadow Memory
Vector Clock
スタックトレース記録
実行速度: 2-20倍遅延
理由: 全メモリアクセスのインストルメント
Vector Clock更新
Happens-Beforeチェック
具体例:
通常実行:
time go test
→ 1.2s
-race付き:
time go test -race
→ 8.5s (約7倍遅い)
推奨事項:
✓ CIで-raceテスト実行
✓ 本番環境では無効化
✗ 常時有効化(パフォーマンス低下)
自己確認問題
基礎レベル
中級レベル
上級レベル
実践的思考問題
まとめ
この章では、Goのtestingパッケージの内部動作を機械レベルで学びました。
🔑 重要ポイント:
- testing.T構造体
- カバレッジ計測
- ベンチマーク
- Race Detector
💡 実践的洞察:
- テストはコンパイル時に大きく変換される(カバレッジ、race detector)
- ベンチマークは統計的に信頼できる結果を自動的に求める
- Race detectorは理論的基盤(Vector Clock)に基づく正確な検出を実現
- すべての機能はランタイムオーバーヘッドとのトレードオフで設計されている
- Race detectorは開発・テスト専用(本番では無効化)
- カバレッジ100%は必要条件であって十分条件ではない
- ベンチマークは環境要因(CPU負荷等)に敏感
⚠️ 注意点:
次の章では、Goの標準ライブラリの設計思想と、主要パッケージの内部実装を学びます。