[インデックス 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
関数が何らかの理由でエラーを返し、接続オブジェクトc
がnil
になった場合、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
関数とdnsConfig
、dnsTypeALL
: これらは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
がネットワークの問題などで失敗した場合、c
はnil
値になります。しかし、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
関数が成功し、err
がnil
である(つまり、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
文の実行タイミングと、エラーハンドリングの順序にあります。
-
元のコードの問題点:
c, err := Dial("tcp", "8.8.8.8:53")
defer c.Close()
// ここでc.Close()
がスケジュールされるif err != nil { ... }
この順序では、
Dial
がエラーを返してc
がnil
になったとしても、defer c.Close()
はすでにスケジュールされてしまいます。関数が終了する際に、nil
であるc
に対してClose()
メソッドが呼び出され、ランタイムパニックが発生する可能性がありました。 -
修正後のコードの改善点:
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 Code Review: https://golang.org/cl/12766044
参考にした情報源リンク
- Go言語公式ドキュメント:
defer
文に関する解説 - Go言語公式ドキュメント:
net
パッケージに関する解説 - Go言語公式ドキュメント:
testing
パッケージに関する解説 - TCP/IPおよびDNSプロトコルに関する一般的な技術情報