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

[インデックス 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におけるパニックとリカバリ (panicrecover)

Go言語には、例外処理のメカニズムとして panicrecover があります。

  • panic: プログラムの実行を中断し、現在のGoroutineのスタックをアンワインド(巻き戻し)します。これは、回復不能なエラーや予期せぬ状況が発生した場合に使用されます。パニックが発生すると、通常はプログラム全体が終了します。
  • defer: defer ステートメントは、それが含まれる関数の実行が終了する直前(returnする前、またはパニックが発生してスタックがアンワインドされる前)に、指定された関数を実行することを保証します。
  • recover: recoverdefer された関数内でのみ有効です。recover を呼び出すと、現在のGoroutineで発生したパニックを捕捉し、パニックの値を返します。recover がパニックを捕捉した場合、スタックのアンワインドは停止し、プログラムの実行は recover を呼び出した defer 関数から再開されます。これにより、パニックから回復し、プログラムのクラッシュを防ぐことができます。

このコミットのテストでは、Once.Do に渡された関数が panic を起こした場合に、deferrecover を使ってそのパニックを捕捉し、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 はこの挙動を具体的にテストしています。

  1. once := new(Once) で新しい Once インスタンスを作成します。
  2. ループを2回実行します。
    • 内部で deferrecover を使用して、once.Do 内で発生するパニックを捕捉します。
    • once.Do(func() { panic("failed") }) を呼び出し、意図的にパニックを発生させます。
    • recover()nil でないことを確認し、パニックが期待通りに発生し捕捉されたことを検証します。もしパニックが発生しなければ t.Fatalf でテストを失敗させます。
  3. ループが終了した後、once.Do(func() {}) を呼び出します。この呼び出しは、前のパニックによって Once の状態がリセットされているため、正常に実行されるはずです。
  4. 最後に、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) {

主な変更点は以下の通りです。

  1. 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 関数内でテストエラーを報告できるようになりました。
  2. run 関数内での t.Errorf の追加: once.Do 実行後に *o の値が 1 でない場合にエラーを報告するようになりました。
  3. TestOnce 関数内での go run の呼び出し変更: go run(once, o, c) から go run(t, once, o, c) へと変更されました。
  4. 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言語の公式ドキュメント
  • Go言語のソースコード (特に src/sync/once.gosrc/sync/once_test.go)
  • Go言語の panicrecover に関する一般的な解説記事
  • Go言語のテストに関する一般的な解説記事
  • GitHubのコミット履歴