第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 testコマンドが自動生成するファイル名は何ですか?そのファイルの役割は?
  • testing.T構造体の主要なフィールドを3つ挙げ、それぞれの役割を説明してください。
  • t.Run()で作成されるサブテストのname(テスト名)はどのような形式になりますか?
  • カバレッジ計測時に挿入されるカウンタの型は何ですか?なぜその型が選ばれたのですか?
  • ベンチマークの初期N値はいくつですか?その後どのように調整されますか?
  • 中級レベル

  • _testmain.goファイルにはどのような情報が含まれますか?生成プロセスを3ステップで説明してください。
  • testing.T.Errorf()が内部でバッファリングを行う理由は何ですか?並列テストとの関連を説明してください。
  • 基本ブロックとは何ですか?どのような条件で新しい基本ブロックが始まりますか?
  • ベンチマークでb.ResetTimer()を呼ぶ理由は何ですか?いつ呼ぶべきですか?
  • Vector Clockにおける「Happens-Before」関係とはどういう意味ですか?数式で表現してください。
  • 上級レベル

  • カバレッジプロファイルの"mode: set"、"mode: count"、"mode: atomic"の違いは何ですか?それぞれどのような場合に使用しますか?
  • ベンチマークのb.N決定アルゴリズムは何を目標に調整を行いますか?その目標値はいくつですか?
  • Shadow Memoryの実アドレスからShadowアドレスへの変換式を説明してください。なぜ右シフト3ビットなのですか?
  • Race Detectorが同期プリミティブ(Mutex、チャネル等)をどのようにトラッキングしますか?raceacquire/racereleaseの役割を説明してください。
  • testing.T構造体のcommon.parentフィールドは何のために存在しますか?どのように使用されますか?
  • 実践的思考問題

  • カバレッジ100%を達成すればバグがないと言えますか?カバレッジ計測の限界について説明してください。
  • Race Detectorはなぜ本番環境で使用すべきではないのですか?技術的な理由を3つ挙げてください。
  • 大規模なプロジェクトでベンチマークを安定させるには、どのような工夫が必要ですか?b.N以外の要因を考慮してください。
  • まとめ

    この章では、Goのtestingパッケージの内部動作を機械レベルで学びました。

    🔑 重要ポイント

  • テストバイナリビルド
- _testmain.goの自動生成 - テストレジストリの構築 - ASTベースの関数抽出

  • testing.T構造体
- 階層的なテスト管理 - 出力バッファリング機構 - 並列実行サポート

  • カバレッジ計測
- 基本ブロック識別 - カウンタコード挿入 - コンパイル時のソース変換

  • ベンチマーク
- 適応的なb.N決定 - 線形外挿アルゴリズム - ns/op計算

  • Race Detector
- ThreadSanitizerベース - Vector Clock + Shadow Memory - Happens-Before関係追跡

💡 実践的洞察

  • テストはコンパイル時に大きく変換される(カバレッジ、race detector)
  • ベンチマークは統計的に信頼できる結果を自動的に求める
  • Race detectorは理論的基盤(Vector Clock)に基づく正確な検出を実現
  • すべての機能はランタイムオーバーヘッドとのトレードオフで設計されている
  • ⚠️ 注意点

  • Race detectorは開発・テスト専用(本番では無効化)
  • カバレッジ100%は必要条件であって十分条件ではない
  • ベンチマークは環境要因(CPU負荷等)に敏感

次の章では、Goの標準ライブラリの設計思想と、主要パッケージの内部実装を学びます。