[インデックス 13498] ファイルの概要
このコミットは、Go言語の標準ライブラリであるtesting
パッケージにおける重要な改善を導入しています。具体的には、テスト(T
型)およびベンチマーク(B
型)の実行中に、ログ出力やテストの失敗報告といった操作を並行して行えるようにするための変更です。これにより、並行テストの記述がより安全かつ柔軟になり、特に並行処理を多用するテストケースにおいて、デッドロックやデータ競合のリスクを低減し、テストの信頼性を向上させます。
コミット
commit 9b1412701fc5f265c478153b2032ca90755c38cf
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Jul 25 10:17:27 2012 -0700
testing: allow concurrent use of T and B
Notably, allow concurrent logging and failing.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6453045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9b1412701fc5f265c478153b2032ca90755c38cf
元コミット内容
testing: allow concurrent use of T and B
Notably, allow concurrent logging and failing.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6453045
変更の背景
Go言語のtesting
パッケージは、ユニットテストやベンチマークテストを記述するための標準的なフレームワークを提供します。テスト関数やベンチマーク関数は、それぞれ*testing.T
や*testing.B
のインスタンスを受け取り、これらを通じてログ出力(Log
, Logf
)、エラー報告(Error
, Errorf
, Fail
, FailNow
)、ベンチマーク操作(N
, StopTimer
, StartTimer
など)を行います。
このコミット以前は、testing.T
やtesting.B
の内部状態(特にテストの出力バッファoutput
や失敗フラグfailed
)は、複数のゴルーチンから同時にアクセスされた場合にデータ競合(data race)を引き起こす可能性がありました。例えば、t.Log()
やt.Fail()
といったメソッドが異なるゴルーチンから同時に呼び出された場合、内部の共有データが保護されていないため、予期せぬ動作やクラッシュが発生する可能性がありました。
Goのテストは、しばしば並行処理をテストするために、テスト関数内で複数のゴルーチンを起動します。このようなシナリオでは、各ゴルーチンが独立して*testing.T
のメソッドを呼び出すことが想定されます。しかし、共有リソースへの非同期アクセスが適切に同期されていないと、テスト自体が不安定になり、テスト結果の信頼性が損なわれるという問題がありました。
この変更の背景には、testing
パッケージの堅牢性を高め、並行テストの記述をより安全かつ容易にするという目的があります。特に、並行ログ出力や並行失敗報告を可能にすることで、開発者は並行処理のテストにおいて、*testing.T
や*testing.B
のメソッド呼び出しに関する同期の心配をすることなく、テストロジックに集中できるようになります。
前提知識の解説
Go言語のtesting
パッケージ
Go言語の標準ライブラリに含まれるtesting
パッケージは、Goプログラムの自動テストをサポートします。
*testing.T
: ユニットテスト関数(func TestXxx(t *testing.T)
)に渡される型です。テストのログ出力、エラー報告、テストのスキップなどの機能を提供します。*testing.B
: ベンチマークテスト関数(func BenchmarkXxx(b *testing.B)
)に渡される型です。コードのパフォーマンスを測定するための機能を提供します。Fail()
: テストを失敗としてマークしますが、テスト関数の実行は継続します。Failed()
: テストが失敗としてマークされているかどうかを返します。Log()
/Logf()
: テストの実行中にメッセージを出力します。これらのメッセージは、テストが失敗した場合や、-v
フラグが指定された場合に表示されます。
Go言語の並行処理と同期プリミティブ
Go言語は、ゴルーチン(goroutine)とチャネル(channel)という強力なプリミティブを用いて並行処理をサポートします。しかし、複数のゴルーチンが共有メモリにアクセスする際には、データ競合を防ぐための同期メカニズムが必要です。
- データ競合 (Data Race): 複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生します。データ競合は予測不能な結果やプログラムのクラッシュを引き起こす可能性があります。
sync
パッケージ: Goの標準ライブラリsync
パッケージは、並行処理における同期プリミティブを提供します。sync.Mutex
: 排他ロック(mutual exclusion lock)を提供します。Lock()
メソッドでロックを取得し、Unlock()
メソッドでロックを解放します。一度に1つのゴルーチンのみがロックを保持できます。主に書き込み操作の保護に使用されます。sync.RWMutex
: 読み書きロック(reader-writer mutex)を提供します。RLock()
/RUnlock()
: 読み込みロックを取得/解放します。複数のゴルーチンが同時に読み込みロックを保持できます。Lock()
/Unlock()
: 書き込みロックを取得/解放します。書き込みロックが保持されている間は、他の読み込みロックも書き込みロックも取得できません。RWMutex
は、読み込み操作が頻繁で書き込み操作が少ない場合に、Mutex
よりも高い並行性を提供できます。
技術的詳細
このコミットの核心は、testing
パッケージの内部構造であるcommon
型にsync.RWMutex
を導入し、共有される状態(output
とfailed
)へのアクセスを同期することです。
common
型は、*testing.T
と*testing.B
の両方に共通する基盤となる構造体であり、テストの出力バッファ(output []byte
)とテストの失敗状態(failed bool
)を保持しています。これらのフィールドは、Fail()
, Failed()
, log()
などのメソッドによって読み書きされます。
変更前は、これらのフィールドへのアクセスは同期されていませんでした。そのため、複数のゴルーチンが同時にこれらのメソッドを呼び出すと、データ競合が発生する可能性がありました。
このコミットでは、以下の変更が行われました。
-
sync.RWMutex
の追加:common
構造体にmu sync.RWMutex
フィールドが追加されました。このミューテックスは、output
とfailed
フィールドへのアクセスを保護するために使用されます。type common struct { mu sync.RWMutex // guards output and failed output []byte // Output generated by test or benchmark. failed bool // Test or benchmark has failed. // ... }
-
Fail()
メソッドの変更:Fail()
メソッドは、c.failed = true
という書き込み操作を行うため、mu.Lock()
とmu.Unlock()
で保護されるようになりました。これにより、複数のゴルーチンが同時にFail()
を呼び出しても、failed
フィールドへの書き込みが排他的に行われ、データ競合が防止されます。func (c *common) Fail() { c.mu.Lock() defer c.mu.Unlock() c.failed = true }
-
Failed()
メソッドの変更:Failed()
メソッドは、c.failed
という読み込み操作を行うため、mu.RLock()
とmu.RUnlock()
で保護されるようになりました。RWMutex
の特性により、複数のゴルーチンが同時にFailed()
を呼び出すことができ、読み込み操作の並行性が維持されます。func (c *common) Failed() bool { c.mu.RLock() defer c.mu.RUnlock() return c.failed }
-
log()
メソッドの変更:log()
メソッドは、c.output
にデータを追加する書き込み操作を行うため、mu.Lock()
とmu.Unlock()
で保護されるようになりました。これにより、並行してログ出力が行われても、output
バッファへの書き込みが安全に行われます。func (c *common) log(s string) { c.mu.Lock() defer c.mu.Unlock() c.output = append(c.output, decorate(s)...) }
-
report()
およびRunTests()
でのFailed()
の使用:t.failed
への直接アクセスが、同期されたt.Failed()
メソッドの呼び出しに置き換えられました。これにより、テスト結果の報告時にもfailed
状態への安全な読み込みが保証されます。
これらの変更により、*testing.T
や*testing.B
のインスタンスが複数のゴルーチン間で共有され、同時にメソッドが呼び出された場合でも、内部状態の整合性が保たれ、データ競合が回避されるようになりました。特に、ログ出力やテストの失敗報告といった操作が並行して行えるようになったことで、並行処理をテストする際の柔軟性と信頼性が大幅に向上しました。
コアとなるコードの変更箇所
src/pkg/testing/testing.go
ファイルが変更されています。
--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -87,6 +87,7 @@ import (
"runtime/pprof"
"strconv"
"strings"
+ "sync" // 追加
"time"
)
@@ -116,8 +117,10 @@ var (
// common holds the elements common between T and B and
// captures common methods such as Errorf.
type common struct {
- output []byte // Output generated by test or benchmark.
- failed bool // Test or benchmark has failed.
+ mu sync.RWMutex // guards output and failed // 追加
+ output []byte // Output generated by test or benchmark.
+ failed bool // Test or benchmark has failed.
+
start time.Time // Time test or benchmark started
duration time.Duration
self interface{} // To be sent on signal channel when done.
@@ -176,10 +179,18 @@ type T struct {
}
// Fail marks the function as having failed but continues execution.
-func (c *common) Fail() { c.failed = true }
+func (c *common) Fail() {
+ c.mu.Lock() // 追加
+ defer c.mu.Unlock() // 追加
+ c.failed = true
+}
// Failed returns whether the function has failed.
-func (c *common) Failed() bool { return c.failed }
+func (c *common) Failed() bool {
+ c.mu.RLock() // 追加
+ defer c.mu.RUnlock() // 追加
+ return c.failed
+}
// FailNow marks the function as having failed and stops its execution.
// Execution will continue at the next test or benchmark.
@@ -210,6 +221,8 @@ func (c *common) FailNow() {
// log generates the output. It's always at the same stack depth.
func (c *common) log(s string) {
+ c.mu.Lock() // 追加
+ defer c.mu.Unlock() // 追加
c.output = append(c.output, decorate(s)...)
}
@@ -303,7 +316,7 @@ func Main(matchString func(pat, str string) (bool, error), tests []InternalTest,
func (t *T) report() {
tstr := fmt.Sprintf("(%.2f seconds)", t.duration.Seconds())
format := "--- %s: %s %s\n%s"
- if t.failed { // 変更
+ if t.Failed() { // 変更
fmt.Printf(format, "FAIL", t.name, tstr, t.output)
} else if *chatty {
fmt.Printf(format, "PASS", t.name, tstr, t.output)
@@ -362,7 +375,7 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
continue
}
t.report()
- ok = ok && !out.failed // 変更
+ ok = ok && !out.Failed() // 変更
}
running := 0
@@ -375,7 +388,7 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
}
t := (<-collector).(*T)
t.report()
- ok = ok && !t.failed // 変更
+ ok = ok && !t.Failed() // 変更
running--
}
}
コアとなるコードの解説
このコミットの主要な変更点は、testing
パッケージのcommon
構造体にsync.RWMutex
(読み書きミューテックス)を追加し、このミューテックスを使って共有される状態(output
とfailed
)へのアクセスを同期したことです。
-
import "sync"
の追加:sync
パッケージがインポートされ、sync.RWMutex
を使用できるようになりました。 -
common
構造体へのmu sync.RWMutex
の追加:common
構造体は、*testing.T
と*testing.B
が内部的に共有する基盤となる構造体です。ここにmu sync.RWMutex
が追加され、output
(テスト出力バッファ)とfailed
(テスト失敗フラグ)という2つのフィールドを保護する役割を担います。 -
Fail()
メソッドの変更:- 変更前:
func (c *common) Fail() { c.failed = true }
- 変更後:
func (c *common) Fail() { c.mu.Lock(); defer c.mu.Unlock(); c.failed = true }
Fail()
はc.failed
というブール値をtrue
に設定する書き込み操作です。複数のゴルーチンが同時にFail()
を呼び出すと、c.failed
へのデータ競合が発生する可能性があります。c.mu.Lock()
とdefer c.mu.Unlock()
を追加することで、この操作が排他的に行われるようになり、failed
フィールドの整合性が保証されます。
- 変更前:
-
Failed()
メソッドの変更:- 変更前:
func (c *common) Failed() bool { return c.failed }
- 変更後:
func (c *common) Failed() bool { c.mu.RLock(); defer c.mu.RUnlock(); return c.failed }
Failed()
はc.failed
の値を読み込む操作です。読み込み操作は、書き込み操作と競合しない限り、複数のゴルーチンが同時に行っても安全です。c.mu.RLock()
とdefer c.mu.RUnlock()
を使用することで、複数のゴルーチンが同時にFailed()
を呼び出してfailed
の状態を読み取ることが可能になり、読み込みの並行性が向上します。これはsync.RWMutex
の大きな利点です。
- 変更前:
-
log()
メソッドの変更:- 変更前:
func (c *common) log(s string) { c.output = append(c.output, decorate(s)...) }
- 変更後:
func (c *common) log(s string) { c.mu.Lock(); defer c.mu.Unlock(); c.output = append(c.output, decorate(s)...) }
log()
はc.output
というバイトスライスにデータを追加する書き込み操作です。スライスへのappend
は、基盤となる配列の再割り当てや要素のコピーを伴う可能性があり、複数のゴルーチンから同時に行われるとデータ競合を引き起こします。c.mu.Lock()
とdefer c.mu.Unlock()
で保護することで、ログ出力が排他的に行われ、output
バッファの整合性が保たれます。
- 変更前:
-
report()
およびRunTests()
でのt.Failed()
の利用: 以前はt.failed
フィールドに直接アクセスしていましたが、この変更により、t.Failed()
メソッドを介してアクセスするようになりました。これにより、failed
状態の読み込みもsync.RWMutex
によって適切に同期されるようになり、テスト結果の報告時にも安全性が確保されます。
これらの変更により、*testing.T
や*testing.B
のインスタンスが複数のゴルーチン間で共有され、同時にメソッドが呼び出された場合でも、内部状態の整合性が保たれ、データ競合が回避されるようになりました。特に、ログ出力やテストの失敗報告といった操作が並行して行えるようになったことで、並行処理をテストする際の柔軟性と信頼性が大幅に向上しました。
関連リンク
- Go CL 6453045: https://golang.org/cl/6453045
参考にした情報源リンク
- Go言語の
sync
パッケージドキュメント: https://pkg.go.dev/sync - Go言語の
testing
パッケージドキュメント: https://pkg.go.dev/testing - Go言語における並行処理の概念(ゴルーチン、チャネル、ミューテックスなど)に関する一般的な情報源(例: Go言語の公式ドキュメント、Effective Goなど)