Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 10898] ファイルの概要

このコミットは、Go言語のtestingパッケージにおいて、ベンチマークに対してテストと同様の出力機能とエラーハンドリング機能を提供するための大規模なリファクタリングを行ったものです。Rob Pikeによる2011年12月20日のコミットで、testingパッケージの内部構造を劇的に変更し、TとBの両方で共通のメソッドを利用できるようにしました。

コミット

commit c50e4f5e2f260ff2b68a127843b197443460043f
Author: Rob Pike <r@golang.org>
Date:   Tue Dec 20 09:51:39 2011 -0800

    testing: allow benchmarks to print and fail
    Refactors the benchmarks and test code.
    Now benchmarks can call Errorf, Fail, etc.,
    and the runner will act accordingly.
    
    Because functionality has been folded into an
    embedded type, a number of methods' docs
    no longer appear in godoc output. A fix is
    underway; if it doesn't happen fast enough,
    I'll add wrapper methods to restore the
    documentation.
    
    R=bradfitz, adg, rsc
    CC=golang-dev
    https://golang.org/cl/5492060

変更対象:

  • src/pkg/testing/benchmark.go (71行追加)
  • src/pkg/testing/testing.go (110行変更)
  • test/fixedbugs/bug229.go (4行変更)

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/c50e4f5e2f260ff2b68a127843b197443460043f

元コミット内容

このコミットは以下の主要な変更を行っています:

  1. 共通構造体(common struct)の導入: testing.Ttesting.Bの両方で使用される共通フィールドとメソッドを含むcommon構造体を新しく作成
  2. 埋め込み型(embedded type)の活用: TとBの両方にcommonを埋め込むことで、コードの重複を削減
  3. ベンチマーク機能の拡張: ベンチマークでのエラーハンドリングと出力機能の追加
  4. ゴルーチンベースの実行: ベンチマークを別のゴルーチンで実行する仕組みの導入

変更の背景

このリファクタリングが行われた背景には、以下の課題がありました:

1. コードの重複問題

Go 1.0以前のtestingパッケージでは、testing.T(テスト用)とtesting.B(ベンチマーク用)が似たような機能を持ちながら、それぞれ独立してメソッドを実装していました。これにより、同じような機能が重複して実装されており、保守性が低下していました。

2. ベンチマークの機能制限

従来のベンチマークでは、テストのようにErrorfFailFatalなどのメソッドを使用できませんでした。これにより、ベンチマーク実行中にエラーが発生した場合の適切な処理が困難でした。

3. 出力機能の不足

ベンチマークでは、テストのようにログ出力や詳細な情報を提供する機能が不足していました。デバッグや問題解決において、この制限は大きな問題となっていました。

4. API設計の一貫性

テストとベンチマークで異なるAPIを提供することは、開発者の使い勝手を悪くし、学習コストを増大させていました。

前提知識の解説

1. Goの埋め込み型(Embedded Types)

Goでは、構造体に他の型を埋め込むことで、埋め込まれた型のメソッドとフィールドを直接利用できます。これは継承に似た機能ですが、より明示的で制御されたアプローチです。

type common struct {
    field1 string
    field2 int
}

func (c *common) Method1() {}

type T struct {
    common  // 埋め込み
    extraField string
}

// TはMethod1()を直接呼び出せる

2. Goのtestingパッケージ

Goの標準的なテストフレームワークで、以下の機能を提供します:

  • 単体テスト(TestXxx関数)
  • ベンチマーク(BenchmarkXxx関数)
  • 例題テスト(ExampleXxx関数)

3. チャンネルによる同期

Goでは、チャンネルを使用してゴルーチン間での同期と通信を行います。このコミットでは、テストとベンチマークの実行制御にチャンネルを活用しています。

4. リファクタリングの原則

  • DRY原則: Don't Repeat Yourself - 同じコードを繰り返さない
  • Single Responsibility: 各構造体やメソッドは単一の責任を持つ
  • Composition over Inheritance: 継承よりも合成を選ぶ

技術的詳細

1. common構造体の設計

type common struct {
    output   []byte           // テストまたはベンチマークで生成された出力
    failed   bool             // テストまたはベンチマークが失敗したかどうか
    start    time.Time        // テストまたはベンチマークの開始時刻
    duration time.Duration    // 実行時間
    self     interface{}      // 完了時にsignalチャンネルに送信される自分自身
    signal   chan interface{} // シリアルテスト用の出力チャンネル
}

この構造体は、テストとベンチマークの両方で必要な共通の状態を管理します。

2. メソッドの共有化

以下のメソッドがcommon構造体に移行されました:

func (c *common) Fail() { c.failed = true }
func (c *common) Failed() bool { return c.failed }
func (c *common) FailNow() {
    c.duration = time.Now().Sub(c.start)
    c.Fail()
    c.signal <- c.self
    runtime.Goexit()
}

func (c *common) Log(args ...interface{}) { c.log(fmt.Sprintln(args...)) }
func (c *common) Logf(format string, args ...interface{}) { c.log(fmt.Sprintf(format, args...)) }
func (c *common) Error(args ...interface{}) {
    c.log(fmt.Sprintln(args...))
    c.Fail()
}
func (c *common) Errorf(format string, args ...interface{}) {
    c.log(fmt.Sprintf(format, args...))
    c.Fail()
}

3. ベンチマーク実行の改善

ベンチマークの実行が以下のように変更されました:

func (b *B) run() BenchmarkResult {
    go b.launch()    // 別のゴルーチンで実行
    <-b.signal       // 完了を待機
    return b.result
}

4. エラーハンドリングの統合

ベンチマークでもテストと同様のエラーハンドリングが可能になりました:

for !b.failed && b.duration < d && n < 1e9 {
    // ベンチマークが失敗した場合、ループを終了
    // ...
}

5. 出力管理の改善

ベンチマークの出力が適切に管理されるようになりました:

func (b *B) trimOutput() {
    const maxNewlines = 10
    for nlCount, j := 0, 0; j < len(b.output); j++ {
        if b.output[j] == '\n' {
            nlCount++
            if nlCount >= maxNewlines {
                b.output = append(b.output[:j], "\n\t... [output truncated]\n"...)
                break
            }
        }
    }
}

コアとなるコードの変更箇所

1. T構造体の変更

変更前:

type T struct {
    name          string        // テスト名
    errors        string        // エラー文字列
    failed        bool          // テストが失敗したかどうか
    ch            chan *T       // シリアルテスト用の出力
    startParallel chan bool     // 並列テストの待機用
    start         time.Time     // テスト開始時刻
    dt            time.Duration // テスト時間
}

変更後:

type T struct {
    common                      // 共通機能を埋め込み
    name          string        // テスト名
    startParallel chan bool     // 並列テストの待機用
}

2. B構造体の変更

変更前:

type B struct {
    N         int
    benchmark InternalBenchmark
    ns        time.Duration
    bytes     int64
    start     time.Time
    timerOn   bool
}

変更後:

type B struct {
    common                      // 共通機能を埋め込み
    N         int
    benchmark InternalBenchmark
    bytes     int64
    timerOn   bool
    result    BenchmarkResult
}

3. run()メソッドの分割

変更前:

func (b *B) run() BenchmarkResult {
    // 直接ベンチマークを実行
    // ...
    return BenchmarkResult{b.N, b.ns, b.bytes}
}

変更後:

func (b *B) run() BenchmarkResult {
    go b.launch()    // 別のゴルーチンで起動
    <-b.signal       // 完了を待機
    return b.result
}

func (b *B) launch() {
    // 実際のベンチマーク実行
    // ...
    b.result = BenchmarkResult{b.N, b.duration, b.bytes}
    b.signal <- b   // 完了を通知
}

コアとなるコードの解説

1. common構造体の設計思想

type common struct {
    output   []byte           // 出力バッファ
    failed   bool             // 失敗フラグ
    start    time.Time        // 開始時刻
    duration time.Duration    // 実行時間
    self     interface{}      // 自己参照
    signal   chan interface{} // 通信チャンネル
}

この構造体は、テストとベンチマークの共通の状態を管理します。selfフィールドは型安全性を保ちながら、チャンネル経由で自分自身を送信するために使用されます。

2. 埋め込み型による機能共有

type T struct {
    common  // この行により、Tはcommonの全てのメソッドを使用可能
    name          string
    startParallel chan bool
}

埋め込み型により、Tcommonの全てのメソッド(Fail(), Error(), Log()など)を直接呼び出すことができます。

3. ゴルーチンベースの実行制御

func (b *B) run() BenchmarkResult {
    go b.launch()    // 非同期実行
    <-b.signal       // 完了待機
    return b.result
}

この設計により、ベンチマークの実行を制御しやすくなり、エラーハンドリングや出力管理が統一されました。

4. 失敗時の処理

for !b.failed && b.duration < d && n < 1e9 {
    // ベンチマークが失敗した場合、ループを中断
    // ...
}

if b.failed {
    fmt.Printf("--- FAIL: %s\n%s", benchName, b.output)
    continue
}

ベンチマークが失敗した場合、適切にエラーメッセージが出力され、実行が停止されます。

5. 出力のトリミング

func (b *B) trimOutput() {
    const maxNewlines = 10
    for nlCount, j := 0, 0; j < len(b.output); j++ {
        if b.output[j] == '\n' {
            nlCount++
            if nlCount >= maxNewlines {
                b.output = append(b.output[:j], "\n\t... [output truncated]\n"...)
                break
            }
        }
    }
}

ベンチマークの出力が長くなりすぎる場合、自動的にトリミングされます。これにより、出力が読みやすくなります。

関連リンク

参考にした情報源リンク