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

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

このコミットは、Go言語の標準ライブラリnetパッケージ内のテストコードにおけるエラーハンドリングの改善を目的としています。具体的には、テスト関数内で起動されたゴルーチン(非テスト関数ゴルーチン)内でtesting.Fatalfの代わりにtesting.Errorfを使用するように変更されています。これにより、テストの実行がより堅牢になり、予期せぬテストのハングアップを防ぐことができます。

コミット

net: make use of testing.Errorf instead of testing.Fatalf in non-test function goroutines

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

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

元コミット内容

commit f0433e422b3b71df54957fcd5ab1db31b02e58d4
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Sat Mar 15 13:43:02 2014 +0900

    net: make use of testing.Errorf instead of testing.Fatalf in non-test function goroutines
    
    See testing.FailNow for further information.
    
    LGTM=iant
    R=golang-codereviews, iant
    CC=golang-codereviews
    https://golang.org/cl/75900043

変更の背景

Goのtestingパッケージには、テスト中にエラーを報告するためのいくつかのメソッドが用意されています。t.Fatalt.Fatalfは、エラーを報告した後に現在のテストゴルーチンを即座に終了させる(runtime.Goexitを呼び出す)という挙動をします。一方、t.Errort.Errorfはエラーを報告するだけで、テストゴルーチンの実行は継続されます。

問題は、t.Fatalt.Fatalfがテスト関数自体(func TestXxx(t *testing.T))から直接呼び出される場合は期待通りにテストを終了させるのですが、テスト関数内で別途起動されたゴルーチン(例えばgo func() { ... }())から呼び出された場合に発生します。この場合、t.Fatalfは呼び出し元のゴルーチンのみを終了させ、テスト関数を実行しているメインのゴルーチンは終了させません。もしメインのゴルーチンが終了したゴルーチンの結果を待っている場合、デッドロックやテストのハングアップが発生する可能性があります。

このコミットは、このような潜在的なテストのハングアップを防ぐために、非テスト関数ゴルーチン内でのt.Fatalfの使用をt.Errorfに置き換えることを目的としています。これにより、エラーが発生してもゴルーチンは終了せず、テストのメインゴルーチンが適切に処理を続行できるようになります。

前提知識の解説

  • Goのテスト (testingパッケージ): Go言語には標準でテストをサポートするtestingパッケージが用意されています。テスト関数はfunc TestXxx(t *testing.T)という形式で定義され、t *testing.Tオブジェクトを通じてテストの成否を報告したり、ログを出力したりします。
  • ゴルーチン (Goroutines): Goの軽量な並行処理の単位です。goキーワードを使って関数を別のゴルーチンとして実行できます。ゴルーチンはOSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。
  • testing.T.Fatal / testing.T.Fatalf: これらのメソッドは、テスト中に致命的なエラーが発生した場合に呼び出されます。エラーメッセージをログに出力し、現在のテストゴルーチンを即座に終了させます。内部的にはruntime.Goexitを呼び出します。
  • testing.T.Error / testing.T.Errorf: これらのメソッドは、テスト中にエラーが発生した場合に呼び出されます。エラーメッセージをログに出力しますが、現在のテストゴルーチンの実行は継続されます
  • runtime.Goexit: この関数は、現在のゴルーチンを終了させます。ただし、プログラム全体を終了させるわけではありません。main関数が終了するか、他の実行中のゴルーチンがなくなると、プログラムは終了します。テストの文脈では、t.Fatalfがこれを呼び出すことで、そのゴルーチンだけが終了します。
  • testing.FailNow: testing.Tのメソッドで、t.Fatalt.Fatalfが内部的に呼び出すものです。このメソッドは、現在のテストゴルーチンを終了させ、テストを失敗としてマークします。重要なのは、これが呼び出し元のゴルーチンのみに影響するという点です。

技術的詳細

このコミットの技術的な核心は、testing.Tのメソッドがゴルーチン内でどのように振る舞うかを理解し、それに基づいてテストコードを修正することにあります。

t.Fatalfが非テスト関数ゴルーチン内で呼び出されると、そのゴルーチンはruntime.Goexitによって終了します。しかし、テストのメインゴルーチンは引き続き実行されます。もしメインゴルーチンが、終了したはずのゴルーチンからの結果を待っていたり、そのゴルーチンがクリーンアップ処理を行うことを期待していたりする場合、メインゴルーチンは永遠に待ち続け、テストがハングアップする可能性があります。これは、テストスイート全体が停止する原因となり、CI/CDパイプラインなどで問題を引き起こす可能性があります。

一方、t.Errorfはエラーを報告するだけで、ゴルーチンを終了させません。これにより、エラーが発生してもゴルーチンは最後まで実行され、メインゴルーチンがそのゴルーチンの終了を待つことができるようになります。エラーはtesting.Tオブジェクトに記録され、テストの最後にまとめて報告されるため、テストの失敗は適切に検出されます。

この変更は、特に並行処理を多用するネットワーク関連のテストにおいて重要です。netパッケージのテストでは、サーバーとクライアントのゴルーチンを起動して通信をテストするパターンが頻繁に登場します。これらのゴルーチン内でt.Fatalfが使われていると、通信エラーが発生した場合にゴルーチンが途中で終了し、テストがデッドロックに陥るリスクがありました。t.Errorfへの変更により、このようなリスクが軽減され、テストの信頼性が向上します。

また、t.Errorfに変更した箇所では、エラー報告後にreturnステートメントを追加しています。これは、t.Errorfがゴルーチンを終了させないため、エラーが報告された後もそのゴルーチン内の後続のコードが実行されてしまうのを防ぐためです。これにより、エラー発生後の不必要な処理や、さらなるエラーの連鎖を防ぎ、テストの意図を明確に保つことができます。

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

このコミットでは、以下のファイルが変更されています。

  • src/pkg/net/fd_mutex_test.go
  • src/pkg/net/net_test.go
  • src/pkg/net/tcp_test.go
  • src/pkg/net/timeout_test.go

これらのファイル内で、t.Fatalまたはt.Fatalfgo func() { ... }()のような非テスト関数ゴルーチン内で使用されている箇所が、t.Errorまたはt.Errorfに置き換えられ、さらにreturnステートメントが追加されています。

例:

--- a/src/pkg/net/fd_mutex_test.go
+++ b/src/pkg/net/fd_mutex_test.go
@@ -63,7 +63,8 @@ func TestMutexCloseUnblock(t *testing.T) {
 	for i := 0; i < 4; i++ {
 		go func() {
 			if mu.RWLock(true) {
-				t.Fatal("broken")
+				t.Error("broken")
+				return
 			}
 			c <- true
 		}()

コアとなるコードの解説

上記の差分は、fd_mutex_test.go内のTestMutexCloseUnblock関数の一部です。このテストでは、複数のゴルーチンを起動し、mu.RWLock(true)の呼び出し結果をチェックしています。

変更前は、mu.RWLock(true)trueを返した場合(これはテストの意図に反する「壊れた」状態)、t.Fatal("broken")が呼び出されていました。これにより、このゴルーチンは即座に終了します。しかし、メインのテストゴルーチンは他のゴルーチンの完了を待っている可能性があり、このゴルーチンが予期せず終了することで、テストがハングアップする可能性がありました。

変更後は、t.Error("broken")が呼び出されます。これにより、エラーはtesting.Tオブジェクトに記録されますが、このゴルーチンは終了しません。その代わりに、returnステートメントが追加されており、エラーが報告された後にゴルーチンがそれ以上処理を進めないように明示的に終了させています。これにより、テストの失敗は適切に記録されつつ、メインのテストゴルーチンがデッドロックに陥るリスクがなくなります。

同様の変更が、net_test.gotcp_test.gotimeout_test.go内の複数のテスト関数(TestShutdown, TestShutdownUnix, TestTCPClose, benchmarkTCPConcurrentReadWrite, TestReadWriteDeadline, TestReadDeadlineDataAvailable, TestWriteDeadlineBufferAvailable, TestAcceptDeadlineConnectionAvailable, TestProlongTimeoutなど)にも適用されています。これらのテストは、ネットワーク接続の確立、データの送受信、タイムアウト処理など、並行処理が関わる複雑なシナリオを検証しており、t.Fatalfの誤用がテストの信頼性を損なう可能性がありました。

関連リンク

参考にした情報源リンク

  • Goの公式ドキュメント
  • コミットメッセージに記載されているGo CL (Code Review) リンク: https://golang.org/cl/75900043 (現在はGo Gerritにリダイレクトされます)
  • testing.T.Fatalftesting.T.Errorfの挙動に関する一般的なGoのテストプラクティスに関する情報。