[インデックス 13888] ファイルの概要
このコミットは、Go言語の標準ライブラリ sync
パッケージ内の Once
型のテストファイル src/pkg/sync/once_test.go
に、新たなテストケース TestOncePanic
を追加するものです。このテストは、Once.Do
メソッドに渡される初期化関数がパニックを起こした場合の Once
の挙動を検証することを目的としています。
コミット
commit 801f6e6367e923ce320276adb279b5f0b1ec9bef
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Sep 20 23:29:29 2012 +0400
sync: add Once test with panic
Tests behavior of Once when initialization function panics.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6554047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/801f6e6367e923ce320276adb279b5f0b1ec9bef
元コミット内容
sync: add Once test with panic
Tests behavior of Once when initialization function panics.
変更の背景
sync.Once
は、プログラムの実行中に特定の処理が一度だけ実行されることを保証するためのGo言語のプリミティブです。通常、初期化処理などに使用されます。しかし、Once.Do
に渡される関数がパニックを起こした場合、Once
の内部状態がどうなるか、そしてその後の Do
の呼び出しがどのように振る舞うかは、明確にテストされていませんでした。
このコミットは、このようなエッジケース、特に初期化関数がパニックを起こした場合の Once
の堅牢性と正確な挙動を保証するために、テストカバレッジを向上させることを目的としています。これにより、開発者が Once
を使用する際に、予期せぬパニックが発生しても Once
が正しく動作し続けることを確認できます。
前提知識の解説
sync.Once
sync.Once
は、Go言語の sync
パッケージで提供される構造体で、特定の関数がプログラムの実行中に一度だけ実行されることを保証します。これは、リソースの初期化、設定のロード、シングルトンインスタンスの生成など、一度だけ実行されるべき処理に非常に有用です。
Once
型は Do
メソッドを持ちます。
func (o *Once) Do(f func())
Do
メソッドは引数として func()
型の関数 f
を受け取ります。複数のGoroutineが同時に o.Do(f)
を呼び出した場合でも、f
は一度だけ実行されることが保証されます。他のGoroutineは f
の実行が完了するまでブロックされます。
Goにおけるパニックとリカバリ (panic
と recover
)
Go言語には、例外処理のメカニズムとして panic
と recover
があります。
panic
: プログラムの実行を中断し、現在のGoroutineのスタックをアンワインド(巻き戻し)します。これは、回復不能なエラーや予期せぬ状況が発生した場合に使用されます。パニックが発生すると、通常はプログラム全体が終了します。defer
:defer
ステートメントは、それが含まれる関数の実行が終了する直前(returnする前、またはパニックが発生してスタックがアンワインドされる前)に、指定された関数を実行することを保証します。recover
:recover
はdefer
された関数内でのみ有効です。recover
を呼び出すと、現在のGoroutineで発生したパニックを捕捉し、パニックの値を返します。recover
がパニックを捕捉した場合、スタックのアンワインドは停止し、プログラムの実行はrecover
を呼び出したdefer
関数から再開されます。これにより、パニックから回復し、プログラムのクラッシュを防ぐことができます。
このコミットのテストでは、Once.Do
に渡された関数が panic
を起こした場合に、defer
と recover
を使ってそのパニックを捕捉し、Once
の挙動を検証しています。
Goのテスト (testing
パッケージ)
Go言語には、組み込みのテストフレームワーク testing
パッケージがあります。
func TestXxx(t *testing.T)
: テスト関数はTest
で始まり、*testing.T
型の引数を取ります。t.Errorf(...)
: テストが失敗したことを報告し、エラーメッセージを出力します。テストは続行されます。t.Fatalf(...)
: テストが失敗したことを報告し、エラーメッセージを出力した後、現在のテスト関数を即座に終了させます。
技術的詳細
このコミットで追加された TestOncePanic
関数は、sync.Once
の初期化関数がパニックを起こした場合の挙動を検証します。
sync.Once
のドキュメントや実装によると、Once.Do
に渡された関数がパニックを起こした場合、そのパニックは Do
メソッドの呼び出し元に伝播します。重要なのは、パニックが発生しても Once
の内部状態は「実行済み」とは見なされず、その後の Do
の呼び出しでは再度初期化関数が実行されるという点です。これは、初期化が成功しなかった場合に、再試行の機会を与えるための設計です。
TestOncePanic
はこの挙動を具体的にテストしています。
once := new(Once)
で新しいOnce
インスタンスを作成します。- ループを2回実行します。
- 内部で
defer
とrecover
を使用して、once.Do
内で発生するパニックを捕捉します。 once.Do(func() { panic("failed") })
を呼び出し、意図的にパニックを発生させます。recover()
がnil
でないことを確認し、パニックが期待通りに発生し捕捉されたことを検証します。もしパニックが発生しなければt.Fatalf
でテストを失敗させます。
- 内部で
- ループが終了した後、
once.Do(func() {})
を呼び出します。この呼び出しは、前のパニックによってOnce
の状態がリセットされているため、正常に実行されるはずです。 - 最後に、
once.Do(func() { t.Fatalf("Once called twice") })
を呼び出します。この呼び出しは、前のステップでOnce
が正常に実行されたため、二度目の実行はされないはずです。もし実行されてt.Fatalf
が呼び出された場合、それはOnce
の保証が破られたことを意味し、テストは失敗します。
このテストは、Once
がパニック発生後もその「一度だけ実行」という保証を維持しつつ、パニックを起こした初期化関数を再実行可能にするという、Goの sync.Once
の重要な特性を検証しています。
コアとなるコードの変更箇所
src/pkg/sync/once_test.go
ファイルに以下の変更が加えられています。
--- a/src/pkg/sync/once_test.go
+++ b/src/pkg/sync/once_test.go
@@ -17,8 +17,11 @@ func (o *one) Increment() {
*o++
}
-func run(once *Once, o *one, c chan bool) {
+func run(t *testing.T, once *Once, o *one, c chan bool) {
once.Do(func() { o.Increment() })
+ if v := *o; v != 1 {
+ t.Errorf("once failed inside run: %d is not 1", v)
+ }
c <- true
}
@@ -28,14 +31,34 @@ func TestOnce(t *testing.T) {
c := make(chan bool)
const N = 10
for i := 0; i < N; i++ {
- go run(once, o, c)
+ go run(t, once, o, c)
}
for i := 0; i < N; i++ {
<-c
}
if *o != 1 {
- t.Errorf("once failed: %d is not 1", *o)
+ t.Errorf("once failed outside run: %d is not 1", *o)
+ }
+}
+
+func TestOncePanic(t *testing.T) {
+ once := new(Once)
+ for i := 0; i < 2; i++ {
+ func() {
+ defer func() {
+ if recover() == nil {
+ t.Fatalf("Once.Do() has not panic'ed")
+ }
+ }()
+ once.Do(func() {
+ panic("failed")
+ })
+ }()
}
+ once.Do(func() {})
+ once.Do(func() {
+ t.Fatalf("Once called twice")
+ })
}
func BenchmarkOnce(b *testing.B) {
主な変更点は以下の通りです。
run
関数のシグネチャ変更:func run(once *Once, o *one, c chan bool)
からfunc run(t *testing.T, once *Once, o *one, c chan bool)
へと変更され、*testing.T
引数が追加されました。これにより、run
関数内でテストエラーを報告できるようになりました。run
関数内でのt.Errorf
の追加:once.Do
実行後に*o
の値が1
でない場合にエラーを報告するようになりました。TestOnce
関数内でのgo run
の呼び出し変更:go run(once, o, c)
からgo run(t, once, o, c)
へと変更されました。TestOncePanic
関数の新規追加: これがこのコミットの主要な変更です。
コアとなるコードの解説
TestOncePanic
関数
func TestOncePanic(t *testing.T) {
once := new(Once) // 新しいOnceインスタンスを作成
// 2回ループする。これは、パニック後にOnceがリセットされ、
// 再度初期化関数が実行されることをテストするため。
for i := 0; i < 2; i++ {
func() { // 無名関数でスコープを作成し、deferがこの関数内で動作するようにする
defer func() { // パニックを捕捉するためのdefer関数
if recover() == nil { // recover()がnilならパニックが発生しなかった
t.Fatalf("Once.Do() has not panic'ed") // パニックが期待されたのに発生しなかった場合、テスト失敗
}
}()
once.Do(func() { // Once.Doを呼び出し、内部で意図的にパニックを発生させる
panic("failed")
})
}() // 無名関数を即時実行
}
// 最初の2回の呼び出しでパニックが発生し、Onceの状態がリセットされたため、
// この呼び出しは正常に実行されるはず。
once.Do(func() {})
// 既に一度実行されたので、この呼び出しは何も実行しないはず。
// もし実行されたら、Onceの保証が破られているのでテスト失敗。
once.Do(func() {
t.Fatalf("Once called twice")
})
}
このテストは、sync.Once
の以下の重要な特性を検証しています。
Once.Do
に渡された関数がパニックを起こした場合、そのパニックはDo
の呼び出し元に伝播する。- パニックが発生した後、
Once
の内部状態は「実行済み」とは見なされず、その後のDo
の呼び出しでは、再度初期化関数が実行される機会が与えられる。 - 初期化関数が正常に完了した場合(パニックしなかった場合)、その後の
Do
の呼び出しでは、初期化関数は二度と実行されない。
このテストは、sync.Once
がパニックのような異常な状況下でも、その「一度だけ実行」という核心的な保証をどのように維持し、同時に初期化の再試行を可能にするかを示しています。
関連リンク
- Go言語
sync
パッケージのドキュメント: https://pkg.go.dev/sync - Go言語
sync.Once
のドキュメント: https://pkg.go.dev/sync#Once - Go言語
panic
とrecover
に関する公式ブログ記事 (英語): https://go.dev/blog/defer-panic-and-recover
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
src/sync/once.go
とsrc/sync/once_test.go
) - Go言語の
panic
とrecover
に関する一般的な解説記事 - Go言語のテストに関する一般的な解説記事
- GitHubのコミット履歴