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

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

このコミットは、Go言語の標準ライブラリであるnetパッケージ内のテストファイルsrc/pkg/net/unicast_test.goに対する修正です。具体的には、ネットワークリスナーのテストにおいて、意図しないnilポインタのデリファレンス(参照外し)が発生する可能性があったバグを修正しています。

コミット

commit aad8e954740ee21333f60a673b0b77b2c2718923
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Thu May 31 06:12:24 2012 +0900

    net: fix test to avoid unintentional nil pointer dereference
    
    R=golang-dev, dave, rsc
    CC=golang-dev
    https://golang.org/cl/6248065

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

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

元コミット内容

src/pkg/net/unicast_test.goファイル内のTestWildWildcardListener関数において、netパッケージの各種Listen系関数(Listen, ListenPacket, ListenTCP, ListenUDP, ListenIP)の呼び出し後のエラーハンドリングロジックが誤っていました。

元のコードでは、以下のようなパターンでリスナーをクローズしようとしていました。

if ln, err := Listen("tcp", ""); err != nil { // エラーが発生した場合に
    ln.Close() // lnがnilであるにもかかわらずCloseを呼び出そうとしていた
}

このロジックでは、Listen関数がエラーを返した場合(err != nil)、ln変数にはnilが代入されます。その状態でln.Close()を呼び出すと、nilポインタのデリファレンスが発生し、プログラムがパニック(クラッシュ)する可能性がありました。テストコードであるため、本番環境への直接的な影響はありませんが、テストの実行が不安定になる原因となります。

変更の背景

この変更の背景には、Go言語におけるエラーハンドリングの基本的な原則と、リソースの適切な解放に関する考慮があります。Goでは、関数がエラーを返す場合、通常は成功時の戻り値(この場合はln)はゼロ値(ポインタ型の場合はnil)となります。リソース(この場合はネットワークリスナー)をクローズする操作は、そのリソースが正常に作成された場合にのみ実行されるべきです。

元のテストコードは、エラーが発生した場合にのみClose()を呼び出すという誤った前提に基づいていました。これは、おそらく「エラーが発生したら、そのリソースをクリーンアップする」という意図があったのかもしれませんが、Listen系関数がエラーを返した時点でリソースは正常に作成されていないため、クリーンアップの対象となる有効なリソース(lnオブジェクト)は存在しません。したがって、nilポインタに対するメソッド呼び出しは不正な操作となります。

このコミットは、テストの堅牢性を高め、潜在的なランタイムパニックを回避することを目的としています。

前提知識の解説

Go言語におけるエラーハンドリング

Go言語では、エラーは多値戻り値の最後の値として返されるのが一般的です。慣習として、関数が成功した場合はnilエラーを返し、失敗した場合は非nilのエラー値を返します。

result, err := someFunction()
if err != nil {
    // エラー処理
    return
}
// 成功時の処理

このパターンは、エラーが発生した場合にはそれ以降の処理(特に成功時にのみ有効なresultに対する操作)を行わないようにするために非常に重要です。

nilポインタのデリファレンス

Goにおいて、ポインタがnilであるにもかかわらず、そのポインタが指す値にアクセスしようとしたり、nilポインタに対してメソッドを呼び出したりすると、ランタイムパニックが発生します。これは「nilポインタのデリファレンス」と呼ばれ、プログラムのクラッシュを引き起こす一般的なバグの一つです。

var p *MyStruct // p は nil
p.DoSomething() // ここでパニックが発生する

netパッケージのListen系関数

Goの標準ライブラリnetパッケージは、ネットワーク通信のための基本的な機能を提供します。Listen系関数は、特定のネットワークアドレスとポートで接続を待ち受けるリスナーを作成するために使用されます。

  • net.Listen(network, address string): 指定されたネットワーク(例: "tcp", "udp")とアドレスでリスナーを作成します。
  • net.ListenPacket(network, address string): パケット指向のネットワーク(例: "udp")でリスナーを作成します。
  • net.ListenTCP(net, laddr *TCPAddr): TCPネットワークでリスナーを作成します。
  • net.ListenUDP(net, laddr *UDPAddr): UDPネットワークでリスナーを作成します。
  • net.ListenIP(net, laddr *IPAddr): IPネットワークでリスナーを作成します。

これらの関数は、成功した場合はnet.Listenerインターフェース(またはその具体的な型)とnilエラーを返し、失敗した場合はnilリスナーと非nilエラーを返します。

Close()メソッド

net.ListenerインターフェースにはClose()メソッドがあり、リスナーが使用していたネットワークリソースを解放するために呼び出されます。これは、ファイルディスクリプタやポート番号など、OSが管理するリソースを適切に解放するために非常に重要です。リソースのリークを防ぐため、通常はdeferステートメントと組み合わせて使用されます。

ln, err := net.Listen("tcp", ":8080")
if err != nil {
    log.Fatal(err)
}
defer ln.Close() // 関数が終了する際に必ずCloseが呼ばれる
// リスナーを使った処理

技術的詳細

このコミットの技術的詳細は、Go言語におけるエラーハンドリングの慣習とnilポインタの安全性に集約されます。

元のコードは、if err != nilという条件でln.Close()を呼び出していました。これは、エラーが発生した場合にのみClose()を実行するというロジックです。しかし、Listen関数がエラーを返した場合、戻り値のlnnilになります。Goの設計では、エラーが発生した場合は成功時の戻り値は有効なオブジェクトではない(通常はゼロ値)と見なされます。したがって、nilであるlnに対してClose()メソッドを呼び出すことは、nilポインタのデリファレンスを引き起こし、ランタイムパニックの原因となります。

修正後のコードは、if err == nilという条件でln.Close()を呼び出しています。これは、Listen関数がエラーを返さず、リスナーが正常に作成された場合にのみClose()を実行するというロジックです。この場合、lnは有効なnet.Listenerオブジェクトであるため、Close()メソッドを安全に呼び出すことができます。

この修正は、Go言語のイディオムに則った正しいエラーハンドリングとリソース解放のパターンを適用したものです。リソースは、それが正常に取得・初期化された場合にのみ解放されるべきであり、エラーが発生してリソースが取得できなかった場合には、そもそも解放すべき対象が存在しないという原則に基づいています。

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

変更はsrc/pkg/net/unicast_test.goファイル内のTestWildWildcardListener関数に集中しています。

--- a/src/pkg/net/unicast_test.go
+++ b/src/pkg/net/unicast_test.go
@@ -555,19 +555,19 @@ func TestWildWildcardListener(t *testing.T) {
 		}\n     	}()
 
-	if ln, err := Listen("tcp", ""); err != nil {
-		ln.Close()
-	}
-	if ln, err := ListenPacket("udp", ""); err != nil {
-		ln.Close()
-	}
-	if ln, err := ListenTCP("tcp", nil); err != nil {
-		ln.Close()
-	}
-	if ln, err := ListenUDP("udp", nil); err != nil {
-		ln.Close()
-	}
-	if ln, err := ListenIP("ip:icmp", nil); err != nil {
-		ln.Close()
-	}
+	if ln, err := Listen("tcp", ""); err == nil {
+		ln.Close()
+	}
+	if ln, err := ListenPacket("udp", ""); err == nil {
+		ln.Close()
+	}
+	if ln, err := ListenTCP("tcp", nil); err == nil {
+		ln.Close()
+	}
+	if ln, err := ListenUDP("udp", nil); err == nil {
+		ln.Close()
+	}
+	if ln, err := ListenIP("ip:icmp", nil); err == nil {
+		ln.Close()
+	}
 }

具体的には、以下の5箇所で条件式がerr != nilからerr == nilに変更されています。

  1. Listen("tcp", "") の呼び出し後
  2. ListenPacket("udp", "") の呼び出し後
  3. ListenTCP("tcp", nil) の呼び出し後
  4. ListenUDP("udp", nil) の呼び出し後
  5. ListenIP("ip:icmp", nil) の呼び出し後

コアとなるコードの解説

この修正は非常にシンプルですが、Go言語の安全性と堅牢性を保つ上で極めて重要です。

元のコードの意図は、おそらく「リスナーの作成を試み、もしエラーがなければ(またはエラーがあっても)クローズする」というものだったかもしれません。しかし、err != nilという条件は「エラーが発生した場合」を意味します。このとき、ln変数にはnilが代入されているため、ln.Close()を呼び出すとnilポインタデリファレンスが発生し、テストがパニックで終了する可能性がありました。

修正後のコードでは、条件がerr == nilに変更されています。これは「エラーが発生しなかった場合」、つまりリスナーが正常に作成され、lnが有効なnet.Listenerオブジェクトである場合にのみln.Close()が呼び出されることを意味します。これにより、nilポインタデリファレンスのリスクが完全に排除され、テストがより安定して実行されるようになります。

この変更は、Go言語における「エラーをチェックし、エラーがない場合にのみ成功時の処理を進める」という基本的なエラーハンドリングのベストプラクティスを厳密に適用したものです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語におけるnilポインタの概念に関する一般的な知識
  • Go言語のエラーハンドリングに関する一般的な慣習とベストプラクティス