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

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

このコミットは、Go言語のnetパッケージ内のテストファイルsrc/pkg/net/dnsclient_unix_test.goにおけるTestTCPLookupテストのバグを修正するものです。具体的には、TCP接続の確立に失敗した場合にdefer c.Close()nilポインタに対して呼び出されることによる潜在的なパニックを防ぐため、defer文の配置を修正しています。

コミット

net: fix TestTCPLookup

R=golang-dev, dvyukov, dave
CC=golang-dev
https://golang.org/cl/12766044

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

https://github.com/golang/go/commit/3a93626b49a762867ddedf1033c90747d479b7ec

元コミット内容

diff --git a/src/pkg/net/dnsclient_unix_test.go b/src/pkg/net/dnsclient_unix_test.go
index 0375af5943..e8edc862da 100644
--- a/src/pkg/net/dnsclient_unix_test.go
+++ b/src/pkg/net/dnsclient_unix_test.go
@@ -15,10 +15,10 @@ func TestTCPLookup(t *testing.T) {
 	t.Skip("skipping test to avoid external network")
 	}
 	c, err := Dial("tcp", "8.8.8.8:53")
-	defer c.Close()
 	if err != nil {
 		t.Fatalf("Dial failed: %v", err)
 	}
+	defer c.Close()
 	cfg := &dnsConfig{timeout: 10, attempts: 3}
 	_, err = exchange(cfg, c, "com.", dnsTypeALL)
 	if err != nil {

変更の背景

このコミットは、TestTCPLookupというテストが、特定の条件下で不安定になったり、パニックを引き起こしたりする可能性があったために行われました。問題の根源は、Go言語のdefer文の実行タイミングと、ネットワーク接続の確立におけるエラーハンドリングの順序にありました。

元のコードでは、net.Dial関数を呼び出してTCP接続を試みた直後にdefer c.Close()が記述されていました。defer文は、その行が実行された時点で関数呼び出しをスケジュールしますが、実際の実行は囲む関数がリターンする直前に行われます。

もしDial関数が何らかの理由でエラーを返し、接続オブジェクトcnilになった場合、defer c.Close()nilであるcに対してClose()メソッドを呼び出すようにスケジュールされてしまいます。関数が終了する際にこのスケジュールされた呼び出しが実行されると、nilポインタデリファレンスが発生し、Goランタイムがパニックを起こす可能性がありました。

この修正は、テストの堅牢性を高め、ネットワーク接続の失敗時にもテストが安全に終了するようにするために不可欠でした。

前提知識の解説

このコミットの理解を深めるために、以下の概念について解説します。

  • Go言語のdefer: deferキーワードは、その関数がreturnする直前に実行される関数呼び出しをスケジュールするために使用されます。これは、ファイルハンドルのクローズ、ミューテックスのアンロック、データベース接続の解放など、リソースのクリーンアップ処理を確実に行うためのGoのイディオムです。deferされた関数は、そのdefer文が評価された時点の引数でスケジュールされます。重要なのは、defer文自体はすぐに評価されますが、スケジュールされた関数の実行は、それを囲む関数が終了するまで遅延されるという点です。

  • Go言語のnetパッケージ: Goの標準ライブラリの一部であり、ネットワークI/Oのプリミティブ機能を提供します。TCP/UDP接続、IPアドレスの解決、DNSルックアップなど、様々なネットワーク関連の操作を行うための型と関数が含まれています。

  • DNS (Domain Name System): インターネット上でドメイン名(例: google.com)をIPアドレス(例: 172.217.160.142)に変換するための分散型システムです。DNSは通常、UDPポート53を使用しますが、ゾーン転送や大きな応答の場合にはTCPポート53も使用されます。

  • TCP (Transmission Control Protocol): インターネットプロトコルスイートの主要なプロトコルの一つで、信頼性の高い、順序付けされた、エラーチェックされたバイトストリームを、ネットワーク上のアプリケーション間で提供します。接続指向のプロトコルであり、データ転送を開始する前に「ハンドシェイク」によって接続を確立します。

  • net.Dial関数: netパッケージで提供される関数で、指定されたネットワークアドレスへの接続を確立するために使用されます。例えば、Dial("tcp", "8.8.8.8:53")は、IPアドレス8.8.8.8のポート53に対してTCP接続を試みます。成功するとnet.Connインターフェースを実装する接続オブジェクトを返し、失敗するとエラーを返します。

  • testingパッケージとt.Skip()t.Fatalf(): Goの標準テストフレームワークであるtestingパッケージは、テストの記述と実行をサポートします。

    • t.Skip("reason"): テストをスキップするために使用されます。特定の環境や条件でテストを実行したくない場合に便利です。
    • t.Fatalf("format", args...): テストを失敗させ、その場でテストの実行を停止するために使用されます。エラーメッセージをフォーマットして出力します。
  • exchange関数とdnsConfigdnsTypeALL: これらはnetパッケージの内部またはテストコードで使用されるDNS関連の概念です。

    • exchange関数: DNSクエリを送信し、応答を受信する内部的なヘルパー関数です。
    • dnsConfig: DNSクエリの動作を制御するための設定(例: タイムアウト、再試行回数)を保持する構造体です。
    • dnsTypeALL: DNSクエリで要求するレコードのタイプを指定する定数で、すべての利用可能なレコードタイプを要求することを示します。

技術的詳細

このコミットの技術的な核心は、Goのdefer文のセマンティクスと、エラーハンドリングの順序に関するものです。

元のコードでは、net.Dialの呼び出しと、その結果として返されるエラーのチェックの間にdefer c.Close()が配置されていました。

	c, err := Dial("tcp", "8.8.8.8:53")
	defer c.Close() // ここが問題の箇所
	if err != nil {
		t.Fatalf("Dial failed: %v", err)
	}

このコードのフローでは、Dial関数が実行された直後にdefer c.Close()がスケジュールされます。もしDialがネットワークの問題などで失敗した場合、cnil値になります。しかし、defer c.Close()はすでにスケジュールされているため、関数TestTCPLookupが終了する際に、nilであるcに対してClose()メソッドが呼び出されてしまいます。Goでは、nilポインタに対してメソッドを呼び出すとランタイムパニックが発生します。これはテストの不安定性やクラッシュの原因となります。

修正後のコードでは、defer c.Close()の行が、Dialの呼び出しと、その後のエラーチェックのブロックの後に移動されています。

	c, err := Dial("tcp", "8.8.8.8:53")
	if err != nil {
		t.Fatalf("Dial failed: %v", err)
	}
	defer c.Close() // ここに移動

この変更により、defer c.Close()がスケジュールされるのは、Dial関数が成功し、errnilである(つまり、cが有効な接続オブジェクトである)ことが確認された後になります。 もしDialが失敗した場合、if err != nilブロックが実行され、t.Fatalfによってテストが即座に終了します。この場合、defer c.Close()の行は実行されないため、nilポインタに対するClose()呼び出しは発生せず、パニックが回避されます。

この修正は、リソースの解放を確実に行いつつ、エラー発生時の堅牢性を高めるという、Go言語における一般的なエラーハンドリングとリソース管理のベストプラクティスに沿ったものです。

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

変更はsrc/pkg/net/dnsclient_unix_test.goファイル内のTestTCPLookup関数で行われました。

--- a/src/pkg/net/dnsclient_unix_test.go
+++ b/src/pkg/net/dnsclient_unix_test.go
@@ -15,10 +15,10 @@ func TestTCPLookup(t *testing.T) {
 		t.Skip("skipping test to avoid external network")
 	}
 	c, err := Dial("tcp", "8.8.8.8:53")
-	defer c.Close()
 	if err != nil {
 		t.Fatalf("Dial failed: %v", err)
 	}
+	defer c.Close()
 	cfg := &dnsConfig{timeout: 10, attempts: 3}
 	_, err = exchange(cfg, c, "com.", dnsTypeALL)
 	if err != nil {

具体的には、defer c.Close()の行が、c, err := Dial("tcp", "8.8.8.8:53")の直後から、if err != nil { t.Fatalf(...) }のエラーチェックブロックの直後に移動されました。

コアとなるコードの解説

この変更の核心は、Goのdefer文の実行タイミングと、エラーハンドリングの順序にあります。

  1. 元のコードの問題点: c, err := Dial("tcp", "8.8.8.8:53") defer c.Close() // ここでc.Close()がスケジュールされる if err != nil { ... }

    この順序では、Dialがエラーを返してcnilになったとしても、defer c.Close()はすでにスケジュールされてしまいます。関数が終了する際に、nilであるcに対してClose()メソッドが呼び出され、ランタイムパニックが発生する可能性がありました。

  2. 修正後のコードの改善点: c, err := Dial("tcp", "8.8.8.8:53") if err != nil { t.Fatalf("Dial failed: %v", err) // エラーがあればここでテストが終了 } defer c.Close() // Dialが成功した場合のみ、c.Close()がスケジュールされる

    この修正により、Dialが成功し、cが有効な(nilではない)接続オブジェクトであることが保証された場合にのみdefer c.Close()がスケジュールされるようになります。もしDialが失敗した場合、if err != nilブロックが実行され、t.Fatalfによってテストが中断されるため、defer c.Close()の行は実行されません。これにより、nilポインタデリファレンスによるパニックが回避され、テストの信頼性が向上します。

この変更は、Go言語でリソースを扱う際の重要なパターンを示しています。リソースの解放をdeferで行う場合、そのリソースが有効であることを確認した後にdeferを配置することが、堅牢なコードを書く上で非常に重要です。

関連リンク

参考にした情報源リンク

  • Go言語公式ドキュメント: defer文に関する解説
  • Go言語公式ドキュメント: netパッケージに関する解説
  • Go言語公式ドキュメント: testingパッケージに関する解説
  • TCP/IPおよびDNSプロトコルに関する一般的な技術情報