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

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

このコミットは、Go言語のnet/httpパッケージにおけるテストコードの修正に関するものです。具体的には、TestServeFileContentTypeというテスト関数において、テストの挙動を制御するために使用されていたGoのチャネル(chan)を、HTTPリクエストのクエリパラメータ(override)を使用する方式に変更しています。これにより、テストの信頼性と堅牢性が向上し、以前のコミットで修正されたデータ競合の問題に対するフォローアップとなっています。

コミット

commit 2ebf0de27c8f12517323d8fd57ac99d213259681
Author: David Symonds <dsymonds@golang.org>
Date:   Wed Jan 18 08:28:09 2012 +1100

    net/http: change test to use override param instead of chan.
    
    Follow-on from https://golang.org/cl/5543062.
    
    R=bradfitz, dvyukov
    CC=golang-dev
    https://golang.org/cl/5539071

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

https://github.com/golang/go/commit/2ebf0de27c8f12517323d8fd57ac99d213259681

元コミット内容

net/httpパッケージのテストにおいて、チャネルの代わりにオーバーライドパラメータを使用するように変更。これはhttps://golang.org/cl/5543062のフォローアップである。

変更の背景

このコミットは、https://golang.org/cl/5543062で導入された変更のフォローアップとして行われました。元のコミット(CL 5543062)は「net/http: fix data race in test」と題されており、テストにおけるデータ競合の修正を目的としていました。

データ競合は、複数のゴルーチン(Goの軽量スレッド)が同時に同じメモリ領域にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する問題です。テストコードにおいても、並行処理が絡むとデータ競合が発生し、テスト結果が非決定論的になったり、誤った結果を報告したりする可能性があります。

TestServeFileContentTypeテストでは、httptest.NewServerを使用してHTTPサーバーを起動し、そのサーバーに対してリクエストを送信してContent-Typeヘッダーの挙動をテストしていました。以前の実装では、テストの内部状態(Content-Typeをオーバーライドするかどうか)を制御するためにGoのチャネル(override := make(chan bool, 1))が使用されていました。しかし、チャネルを介した状態の受け渡しは、テストの並行実行やタイミングによっては、意図しないデータ競合を引き起こす可能性がありました。特に、テストが並行して実行される環境では、チャネルのセマンティクスが複雑になり、デバッグが困難な競合状態を生み出すことがあります。

このコミットの目的は、チャネルによるテスト制御を、HTTPリクエストのクエリパラメータを利用するよりシンプルで堅牢な方法に置き換えることで、テストの信頼性をさらに高めることにありました。HTTPリクエストのパラメータは、各リクエストに固有のコンテキストとして渡されるため、並行実行されるテスト間で状態が混ざり合うリスクが低減されます。

前提知識の解説

Go言語のnet/httpパッケージ

Go言語の標準ライブラリであるnet/httpパッケージは、HTTPクライアントとサーバーの実装を提供します。

  • httptest.NewServer: テスト目的でHTTPサーバーを起動するためのユーティリティ関数です。実際のネットワークポートをリッスンし、テスト対象のhttp.Handlerをラップして、テスト中にHTTPリクエストを送信できるURLを返します。
  • http.HandlerFunc: http.Handlerインターフェースを満たす関数を定義するための型です。これにより、通常の関数をHTTPハンドラとして使用できます。
  • http.ResponseWriter: HTTPレスポンスを構築するためにハンドラが使用するインターフェースです。レスポンスヘッダーの設定(w.Header().Set(...))やレスポンスボディの書き込みを行います。
  • http.Request: 受信したHTTPリクエストを表す構造体です。リクエストメソッド、URL、ヘッダー、ボディなどの情報を含みます。
  • http.Request.FormValue(key string): HTTPリクエストのURLクエリパラメータまたはフォームデータから、指定されたkeyに対応する値を取得するメソッドです。このコミットでは、クエリパラメータからoverrideの値を取得するために使用されています。
  • http.ServeFile(w ResponseWriter, r *Request, name string): 指定されたファイルの内容をHTTPレスポンスとして提供する関数です。ファイルのMIMEタイプを自動的に検出し、Content-Typeヘッダーを設定します。

Go言語のチャネル(chan

チャネルは、Go言語におけるゴルーチン間の通信と同期のための主要なプリミティブです。チャネルを通じて値を送受信することで、ゴルーチンは安全に情報を共有し、実行順序を調整できます。

  • make(chan bool, 1): バッファ付きチャネルを作成します。この場合、バッファサイズは1なので、1つの値をブロックせずに送信できます。2つ目の値を送信しようとすると、受信されるまでブロックされます。
  • <-override: チャネルoverrideから値を受信します。
  • override <- false: チャネルoverrideに値falseを送信します。

テストにおいてチャネルを使用する場合、特に並行テスト環境では、チャネルの送信と受信のタイミングが非決定論的になりやすく、デッドロックやデータ競合の原因となることがあります。

データ競合 (Data Race)

データ競合は、並行プログラミングにおいて発生するバグの一種です。以下の3つの条件がすべて満たされたときに発生します。

  1. 2つ以上のゴルーチンが同時に同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込みである。
  3. アクセスが同期メカニズムによって保護されていない。

データ競合が発生すると、プログラムの動作が予測不能になり、クラッシュしたり、誤った結果を生成したりする可能性があります。テストコードにおいても、データ競合はテストの信頼性を損なうため、避けるべきです。

技術的詳細

このコミットの技術的な核心は、テストの制御フローをチャネルベースの同期から、HTTPリクエストのクエリパラメータベースの非同期制御に移行した点にあります。

変更前:

	override := make(chan bool, 1)
	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
		if <-override { // チャネルから値を受信して制御
			w.Header().Set("Content-Type", ctype)
		}
		ServeFile(w, r, "testdata/file")
	}))
	defer ts.Close()
	get := func(want string) {
		resp, err := Get(ts.URL) // チャネルの状態に依存
		// ...
	}
	override <- false // チャネルに値を送信
	get("text/plain; charset=utf-8")
	override <- true // チャネルに値を送信
	get(ctype)

変更前は、httptest.NewServer内で定義されたハンドラがoverrideチャネルからbool値を受信し、その値に基づいてContent-Typeヘッダーを設定するかどうかを決定していました。テスト関数本体では、overrideチャネルにfalseまたはtrueを送信してからget関数を呼び出すことで、ハンドラの挙動を制御していました。

このアプローチの問題点は、ハンドラとテスト本体がチャネルを介して密結合されており、チャネルの送信と受信のタイミングがテストの並行実行に影響を与える可能性があったことです。特に、複数のテストが同時に実行される場合、チャネルの状態が他のテストに影響を与えたり、デッドロックや競合状態を引き起こしたりするリスクがありました。

変更後:

	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
		if r.FormValue("override") == "1" { // クエリパラメータで制御
			w.Header().Set("Content-Type", ctype)
		}
		ServeFile(w, r, "testdata/file")
	}))
	defer ts.Close()
	get := func(override, want string) {
		resp, err := Get(ts.URL + "?override=" + override) // クエリパラメータを付与
		// ...
	}
	get("0", "text/plain; charset=utf-8") // "override=0"を送信
	get("1", ctype) // "override=1"を送信

変更後では、チャネルが完全に削除され、代わりにHTTPリクエストのクエリパラメータoverrideが使用されています。ハンドラはr.FormValue("override") == "1"という条件で、クエリパラメータoverrideの値が"1"であるかどうかをチェックし、それに基づいてContent-Typeヘッダーを設定します。

テスト関数本体では、get関数がoverrideという文字列パラメータを受け取るようになり、ts.URL + "?override=" + overrideのようにURLに直接クエリパラメータを付与してリクエストを送信します。

この変更の利点は以下の通りです。

  1. データ競合の回避: チャネルを介した同期が不要になるため、並行テスト実行時のデータ競合のリスクが大幅に低減されます。各HTTPリクエストは独立しており、そのリクエストのコンテキスト内で完結するため、テスト間の相互作用が最小限に抑えられます。
  2. テストの独立性: 各テストケースが自身の制御パラメータをHTTPリクエストに含めるため、テストがより独立し、予測可能になります。
  3. コードの簡素化: チャネルの作成、送信、受信といった複雑な同期ロジックが不要になり、テストコードがよりシンプルで読みやすくなります。
  4. HTTPプロトコルへの適合: HTTPリクエストのパラメータを利用することは、HTTPプロトコルの自然な拡張であり、テストの意図がより明確になります。

この修正は、テストの堅牢性を高め、Goの標準ライブラリの品質を維持するために重要な改善と言えます。

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

--- a/src/pkg/net/http/fs_test.go
+++ b/src/pkg/net/http/fs_test.go
@@ -224,16 +224,15 @@ func TestEmptyDirOpenCWD(t *testing.T) {
 
 func TestServeFileContentType(t *testing.T) {
 	const ctype = "icecream/chocolate"
-	override := make(chan bool, 1)
 	ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
-		if <-override {
+		if r.FormValue("override") == "1" {
 			w.Header().Set("Content-Type", ctype)
 		}
 		ServeFile(w, r, "testdata/file")
 	}))
 	defer ts.Close()
-	get := func(want string) {
-		resp, err := Get(ts.URL)
+	get := func(override, want string) {
+		resp, err := Get(ts.URL + "?override=" + override)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -241,10 +240,8 @@ func TestServeFileContentType(t *testing.T) {
 		if h := resp.Header.Get("Content-Type"); h != want {
 			t.Errorf("Content-Type mismatch: got %q, want %q", h, want)
 		}
 	}
-	override <- false
-	get("text/plain; charset=utf-8")
-	override <- true
-	get(ctype)
+	get("0", "text/plain; charset=utf-8")
+	get("1", ctype)
 }
 
 func TestServeFileMimeType(t *testing.T) {

コアとなるコードの解説

上記のdiffは、src/pkg/net/http/fs_test.goファイル内のTestServeFileContentType関数に対する変更を示しています。

  1. チャネルの削除:

    • - override := make(chan bool, 1): overrideという名前のバッファ付きチャネルの宣言と初期化が削除されました。これにより、チャネルを介した同期メカニズムが不要になりました。
  2. ハンドラ内の条件変更:

    • - if <-override {
    • + if r.FormValue("override") == "1" {: HTTPハンドラ内で、チャネルからの値の受信(<-override)から、リクエストのクエリパラメータoverrideの値が文字列"1"であるかどうかのチェック(r.FormValue("override") == "1")に変更されました。これにより、ハンドラの挙動がリクエスト自体に依存するようになりました。
  3. get関数のシグネチャ変更:

    • - get := func(want string) {
    • + get := func(override, want string) {: getというヘルパー関数のシグネチャが変更され、want(期待されるContent-Type)に加えて、overrideという新しい文字列パラメータを受け取るようになりました。このoverrideパラメータが、HTTPリクエストのクエリパラメータとして使用されます。
  4. HTTPリクエストURLの変更:

    • - resp, err := Get(ts.URL)
    • + resp, err := Get(ts.URL + "?override=" + override): get関数内で、HTTPリクエストを送信する際に、ts.URLに直接"?override=" + overrideというクエリ文字列が付加されるようになりました。これにより、overrideパラメータの値がサーバーに渡されます。
  5. get関数の呼び出し変更:

    • - override <- false
    • - get("text/plain; charset=utf-8")
    • - override <- true
    • - get(ctype)
    • + get("0", "text/plain; charset=utf-8")
    • + get("1", ctype): テストの実行部分で、チャネルへの送信が削除され、get関数が新しいシグネチャに合わせて、overrideパラメータとして"0"または"1"を渡すように変更されました。
      • get("0", "text/plain; charset=utf-8"): override=0としてリクエストを送信し、デフォルトのContent-Typetext/plain; charset=utf-8)が返されることを期待します。
      • get("1", ctype): override=1としてリクエストを送信し、カスタムのContent-Typeicecream/chocolate)が返されることを期待します。

これらの変更により、テストの制御がチャネルによる同期からHTTPリクエストのパラメータによる非同期制御に移行し、テストの堅牢性と並行実行時の安全性が向上しました。

関連リンク

  • 元のデータ競合修正コミット: https://golang.org/cl/5543062 (net/http: fix data race in test) - このコミットの背景となった、テストにおけるデータ競合を修正する変更リストです。

参考にした情報源リンク