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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージ内の sniff_test.go ファイルに対する変更です。sniff_test.go は、HTTPコンテンツのContent-Typeを「スニッフィング」(内容から推測)する機能のテストを目的としています。具体的には、io.Copy の結果検証部分で、テスト失敗時のエラー報告方法を t.Fatalf から t.Errorf に変更しています。

コミット

  • コミットハッシュ: 929070ee621603ca0d71ffeae6d2d2893813023f
  • 作者: David Symonds dsymonds@golang.org
  • 日付: Wed Nov 9 16:11:47 2011 +1100
  • コミットメッセージ:
    net/http: use t.Errorf from alternate goroutine in test.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/5348051
    

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

https://github.com/golang/go/commit/929070ee621603ca0d71ffeae6d2d2893813023f

元コミット内容

net/http: use t.Errorf from alternate goroutine in test.

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5348051

変更の背景

このコミットの背景には、Go言語のテストフレームワーク (testing パッケージ) におけるエラー報告の挙動、特にゴルーチンとの相互作用に関する考慮事項があります。

Goのテストでは、*testing.T 型のメソッドを使用してテストの成否を報告します。主なエラー報告メソッドには t.Error / t.Errorft.Fatal / t.Fatalf があります。

  • t.Error / t.Errorf: エラーを報告しますが、テストの実行は継続されます。テスト関数は最後まで実行されます。
  • t.Fatal / t.Fatalf: エラーを報告し、現在のテスト関数を即座に終了させます。これは runtime.Goexit() を呼び出すことで実現され、テスト関数が実行されているゴルーチンを終了させます。

問題は、テスト関数内で別のゴルーチンを起動し、そのゴルーチン内で t.Fatalf を呼び出した場合に発生します。t.Fatalf は呼び出されたゴルーチンを終了させますが、テスト関数が実行されているメインのゴルーチンはそのまま実行を継続しようとします。これにより、テストが予期せぬ状態になったり、デッドロックが発生したり、テストスイート全体が不安定になったりする可能性があります。

このコミットのメッセージにある「from alternate goroutine in test」(テスト内の別のゴルーチンから)という記述は、まさにこのシナリオを指しています。TestContentTypeWithCopy のようなテストでは、HTTPサーバーを起動し、io.Copy を使用してデータを転送するなどの操作が行われます。これらの操作は、内部的に非同期処理や別のゴルーチンを伴う可能性があります。もし io.Copy の結果検証が、テストのメインゴルーチンとは異なるコンテキスト(例えば、HTTPハンドラが実行されているゴルーチンなど)でエラーを検出した場合、そこで t.Fatalf を呼び出すと、テストのメインフローが適切に終了処理を行えなくなるリスクがありました。

t.Fatalft.Errorf に変更することで、エラーが発生してもテスト関数(および関連するゴルーチン)が最後まで実行されるようになります。これにより、テストのクリーンアップ処理が適切に行われたり、より多くのデバッグ情報が収集されたりする機会が生まれます。これは、テストの堅牢性とデバッグのしやすさを向上させるための重要な変更です。

前提知識の解説

Go言語のテストフレームワーク (testing パッケージ)

Go言語には、標準でテストをサポートするための testing パッケージが用意されています。テストファイルは通常、テスト対象のファイルと同じディレクトリに _test.go というサフィックスを付けて配置されます。

  • テスト関数の定義: テスト関数は func TestXxx(t *testing.T) という形式で定義されます。t*testing.T 型のポインタで、テストの状態管理やエラー報告に使用されます。
  • *testing.T の主要なメソッド:
    • t.Error(args ...interface{}): エラーを報告し、テストを失敗としてマークしますが、テスト関数の実行は継続します。
    • t.Errorf(format string, args ...interface{}): t.Error と同様ですが、fmt.Printf と同じ形式でフォーマットされたエラーメッセージを出力できます。
    • t.Fail(): テストを失敗としてマークしますが、実行は継続します。
    • t.FailNow(): テストを失敗としてマークし、現在のテスト関数を即座に終了させます。t.Fatalt.Fatalf の内部で使われます。
    • t.Fatal(args ...interface{}): エラーを報告し、現在のテスト関数を即座に終了させます。
    • t.Fatalf(format string, args ...interface{}): t.Fatal と同様ですが、フォーマットされたエラーメッセージを出力できます。
    • t.Log(args ...interface{}): テスト中に情報を出力します。go test -v オプションで表示されます。
    • t.Logf(format string, args ...interface{}): t.Log と同様ですが、フォーマットされたメッセージを出力できます。

Go言語のゴルーチンと並行処理

Go言語は、軽量な並行処理の仕組みとして「ゴルーチン (goroutine)」を提供します。

  • ゴルーチン: go キーワードを使って関数呼び出しの前に置くことで、その関数を新しいゴルーチンとして実行できます。ゴルーチンはOSのスレッドよりもはるかに軽量で、数千、数万のゴルーチンを同時に実行することが可能です。
  • 並行処理の課題: 複数のゴルーチンが同時に実行される環境では、共有リソースへのアクセス競合や、ゴルーチン間の適切な同期が重要になります。エラーハンドリングも複雑になりがちで、あるゴルーチンで発生したエラーが他のゴルーチンやプログラム全体にどのような影響を与えるかを慎重に考慮する必要があります。

net/http パッケージのコンテンツタイプスニッフィング

net/http パッケージは、HTTPクライアントとサーバーの実装を提供します。このパッケージには、HTTPレスポンスのContent-Typeヘッダが欠落している場合や誤っている場合に、レスポンスボディの先頭数バイトを調べてMIMEタイプを推測する「コンテンツタイプスニッフィング」という機能があります。これは、ブラウザがContent-Typeヘッダに頼らずにコンテンツを正しく表示するために行われるのと同様の処理です。sniff_test.go は、このスニッフィング機能が正しく動作するかを検証するためのテストを含んでいます。

io.Copy 関数

io パッケージは、I/Oプリミティブを提供します。io.Copy(dst io.Writer, src io.Reader) 関数は、src から dst へバイトをコピーします。コピーされたバイト数と、コピー中に発生したエラーを返します。この関数は、ファイル間のコピー、ネットワークストリームの転送など、様々なI/O操作で広く利用されます。

技術的詳細

このコミットの技術的詳細の核心は、t.Fatalft.Errorf の内部的な挙動の違い、そしてそれがゴルーチン環境でどのように影響するかという点にあります。

  1. t.Fatalf の挙動:

    • t.Fatalf は、内部的に t.FailNow() を呼び出します。
    • t.FailNow() は、現在のゴルーチンを終了させるために runtime.Goexit() を呼び出します。
    • runtime.Goexit() は、現在のゴルーチンを終了させますが、panic とは異なり、スタックをアンワインドしません。また、defer 関数は実行されます。
    • テスト関数が実行されているメインのゴルーチンで t.Fatalf が呼ばれた場合、そのテスト関数はそこで終了し、次のテスト関数が実行されます。
    • しかし、テスト関数内で起動された別のゴルーチンt.Fatalf が呼ばれた場合、その「別のゴルーチン」は終了しますが、テスト関数が実行されている「メインのゴルーチン」は終了しません。メインのゴルーチンは、その後の処理を継続しようとします。これにより、テストのロジックが期待する状態と実際の状態が乖離し、デッドロック、リソースリーク、あるいはテストスイート全体のクラッシュといった予期せぬ副作用を引き起こす可能性があります。
  2. t.Errorf の挙動:

    • t.Errorf は、内部的に t.Error() を呼び出し、テストを失敗としてマークするだけです。
    • t.Error() は、t.Fail() を呼び出しますが、t.FailNow() は呼び出しません。
    • したがって、t.Errorf が呼び出されても、現在のゴルーチンやテスト関数は終了せず、その後のコードの実行が継続されます。
  3. この変更の意義:

    • sniff_test.goTestContentTypeWithCopy 関数では、io.Copy を使用してデータをコピーし、その結果を検証しています。この io.Copy の操作は、HTTPサーバーのハンドラ内で実行されるなど、テストのメインゴルーチンとは異なるゴルーチンコンテキストで発生する可能性があります。
    • もし io.Copy が期待通りの結果を返さなかった場合に t.Fatalf が呼ばれていたとすると、そのエラーが発生したゴルーチンは即座に終了しますが、テストのメインゴルーチンはそれを認識せず、テストの残りの部分を実行しようとします。これにより、テストがハングアップしたり、不完全な状態で終了したりする可能性がありました。
    • t.Errorf に変更することで、エラーは適切に報告されるものの、テストの実行は継続されます。これにより、テスト内で起動された他のゴルーチンが正常に終了する機会が与えられ、リソースのクリーンアップが適切に行われる可能性が高まります。また、テストが最後まで実行されることで、単一のエラーでテストが中断されることなく、他の潜在的な問題も検出できるようになり、デバッグの効率が向上します。

この変更は、Goのテストにおける並行処理のベストプラクティスを反映したものであり、テストの信頼性とデバッグのしやすさを向上させるための重要な改善と言えます。

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

変更は src/pkg/net/http/sniff_test.go ファイルの1箇所のみです。

--- a/src/pkg/net/http/sniff_test.go
+++ b/src/pkg/net/http/sniff_test.go
@@ -92,7 +92,7 @@ func TestContentTypeWithCopy(t *testing.T) {
 		buf := bytes.NewBuffer([]byte(input))
 		n, err := io.Copy(w, buf)
 		if int(n) != len(input) || err != nil {
-			t.Fatalf("io.Copy(w, %q) = %v, %v want %d, nil", input, n, err, len(input))
+			t.Errorf("io.Copy(w, %q) = %v, %v want %d, nil", input, n, err, len(input))
 		}
 	}))
 	defer ts.Close()

コアとなるコードの解説

この変更は、TestContentTypeWithCopy 関数内の匿名関数(おそらくHTTPハンドラとして使用される)の内部で行われています。

  1. 元のコード (- 行):

    t.Fatalf("io.Copy(w, %q) = %v, %v want %d, nil", input, n, err, len(input))
    

    この行は、io.Copy の結果を検証しています。io.Copy が期待されるバイト数 (len(input)) をコピーできなかった場合、またはエラー (err != nil) が発生した場合に実行されます。 t.Fatalf は、エラーメッセージをログに出力し、現在のテスト関数(または、この場合はこのコードが実行されているゴルーチン)を即座に終了させます。これにより、テストの残りの部分が実行されなくなり、テストスイート全体に影響を与える可能性がありました。

  2. 変更後のコード (+ 行):

    t.Errorf("io.Copy(w, %q) = %v, %v want %d, nil", input, n, err, len(input))
    

    この行は、t.Fatalft.Errorf に置き換えています。 t.Errorf もエラーメッセージをログに出力し、テストを失敗としてマークしますが、現在のテスト関数(またはゴルーチン)の実行は継続されます。 この変更により、io.Copy でエラーが発生しても、テストの実行フローが中断されることなく、テストの他の部分や、テスト内で起動された他のゴルーチンが最後まで実行されることが保証されます。これは、テストのデバッグ可能性を向上させ、より包括的なエラー報告を可能にするために重要です。例えば、エラーが発生した後も、テストがリソースを適切にクリーンアップしたり、他の関連するアサーションを実行したりできるようになります。

関連リンク

参考にした情報源リンク