[インデックス 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
元コミット内容
このコミットは以下の主要な変更を行っています:
- 共通構造体(common struct)の導入:
testing.T
とtesting.B
の両方で使用される共通フィールドとメソッドを含むcommon
構造体を新しく作成 - 埋め込み型(embedded type)の活用: TとBの両方に
common
を埋め込むことで、コードの重複を削減 - ベンチマーク機能の拡張: ベンチマークでのエラーハンドリングと出力機能の追加
- ゴルーチンベースの実行: ベンチマークを別のゴルーチンで実行する仕組みの導入
変更の背景
このリファクタリングが行われた背景には、以下の課題がありました:
1. コードの重複問題
Go 1.0以前のtestingパッケージでは、testing.T
(テスト用)とtesting.B
(ベンチマーク用)が似たような機能を持ちながら、それぞれ独立してメソッドを実装していました。これにより、同じような機能が重複して実装されており、保守性が低下していました。
2. ベンチマークの機能制限
従来のベンチマークでは、テストのようにErrorf
、Fail
、Fatal
などのメソッドを使用できませんでした。これにより、ベンチマーク実行中にエラーが発生した場合の適切な処理が困難でした。
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
}
埋め込み型により、T
はcommon
の全てのメソッド(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
}
}
}
}
ベンチマークの出力が長くなりすぎる場合、自動的にトリミングされます。これにより、出力が読みやすくなります。
関連リンク
- Go testing package documentation
- Go by Example: Testing and Benchmarking
- How to write benchmarks in Go - Dave Cheney
- Using Subtests and Sub-benchmarks
- Go 101: Type Embedding