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

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

このコミットは、Go言語のnetパッケージにおける接続確立時のエラーハンドリングに関する修正です。具体的には、DialListenListenPacketといったネットワーク操作関数がエラーを返した場合に、戻り値のインターフェース型変数が非nilのポインタを保持してしまう問題を解決します。これにより、エラー発生時に予期せぬ動作やクラッシュを引き起こす可能性があったバグが修正されます。

コミット

commit 66f49f78a5a8f8e6832e8b66eea56387b0c72a52
Author: Russ Cox <rsc@golang.org>
Date:   Fri Oct 18 15:35:45 2013 -0400

    net: make sure failed Dial returns nil Conn
    
    Fixes #6614.
    
    R=golang-dev, bradfitz, mikioh.mikioh
    CC=golang-dev
    https://golang.org/cl/14950045
---
 src/pkg/net/dial.go     | 34 ++++++++++++++++++++++++----------
 src/pkg/net/net_test.go | 38 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 62 insertions(+), 10 deletions(-)

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

https://github.com/golang/go/commit/66f49f78a5a8f8e6832e8b66eea56387b0c72a52

元コミット内容

net: make sure failed Dial returns nil Conn
    
Fixes #6614.
    
R=golang-dev, bradfitz, mikioh.mikioh
CC=golang-dev
https://golang.org/cl/14950045

変更の背景

Go言語では、インターフェース型は内部的に型と値のペアとして表現されます。値がnilであっても、型が非nilである場合、インターフェース自体は非nilと評価されるという特性があります。

このコミット以前のnetパッケージのDialListenListenPacketなどの関数では、接続確立やリスナーの作成に失敗しエラーを返した場合でも、戻り値のConnListenerPacketConnといったインターフェース型変数が、内部的に非nilの具象型(例えば*TCPConn*TCPListener)のnilポインタを保持してしまうことがありました。

これにより、以下のような問題が発生する可能性がありました。

  1. 予期せぬ非nil評価: ユーザーコードがif c != nilのようなチェックを行った際に、エラーが発生しているにもかかわらずインターフェースが非nilと評価され、その後の操作でnilポインタデリファレンスによるパニック(クラッシュ)を引き起こす可能性がありました。
  2. 混乱とデバッグの困難さ: エラーが発生しているのにインターフェースが非nilであるという状況は、開発者にとって混乱を招き、デバッグを困難にしていました。
  3. 一貫性の欠如: Goの慣習として、エラーが返される場合は関連する戻り値はゼロ値(nil)であるべきという期待があり、この挙動はその慣習に反していました。

このコミットは、これらのネットワーク操作関数がエラーを返した場合に、関連するインターフェース型変数が確実にnilとなるように修正することで、Goの慣習に沿った安全で予測可能な挙動を保証することを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とネットワークプログラミングの基礎知識が必要です。

  1. Go言語のインターフェース:

    • Goのインターフェースは、メソッドのシグネチャの集合を定義します。
    • 任意の具象型がインターフェースのすべてのメソッドを実装していれば、その具象型はインターフェース型として扱えます。
    • インターフェース型変数は、内部的に(type, value)のペアとして表現されます。typeは具象型の情報、valueはその具象型の値(ポインタなど)です。
    • インターフェース型変数がnilと評価されるのは、typevalueの両方がnilの場合のみです。
    • もしvaluenilポインタであっても、typeが非nil(例えば*MyStruct)であれば、インターフェース型変数自体はnilとは評価されません。これは、var i interface{} = (*MyStruct)(nil)のような場合にi != nilとなる現象として知られています。
  2. Go言語のnetパッケージ:

    • Goの標準ライブラリの一部で、ネットワークI/O機能を提供します。
    • Connインターフェース: ネットワーク接続を表すインターフェースで、ReadWriteCloseなどのメソッドを持ちます。*TCPConn*UDPConnなどがこのインターフェースを実装します。
    • Listenerインターフェース: 受信接続を待機するリスナーを表すインターフェースで、AcceptCloseなどのメソッドを持ちます。*TCPListener*UnixListenerなどがこのインターフェースを実装します。
    • PacketConnインターフェース: パケット指向のネットワーク接続(UDPなど)を表すインターフェースで、ReadFromWriteToなどのメソッドを持ちます。*UDPConn*IPConnなどがこのインターフェースを実装します。
    • Dial関数: 指定されたネットワークアドレスへの接続を確立します。
    • Listen関数: 指定されたネットワークアドレスで受信接続を待機するリスナーを作成します。
    • ListenPacket関数: 指定されたネットワークアドレスでパケット指向の接続を待機するリスナーを作成します。
  3. エラーハンドリング:

    • Goでは、関数は通常、最後の戻り値としてerror型を返します。
    • エラーが発生した場合、errorは非nilとなり、それ以外の戻り値はゼロ値(数値型なら0、ポインタやインターフェースならnil)となるのが一般的な慣習です。

このコミットは、特にGoのインターフェースの特性と、エラーハンドリングにおける「エラー発生時は関連する戻り値はゼロ値であるべき」という慣習の間の不整合を解消するものです。

技術的詳細

このコミットの技術的詳細は、Goのインターフェースの挙動を考慮し、エラー発生時にインターフェース型変数が確実にnilを返すようにするためのパターンを適用している点にあります。

変更前は、dialSingleListenListenPacketなどの関数内で、具体的な接続やリスナーを作成する内部関数(例: dialTCP, ListenTCP)がエラーを返した場合、そのエラーは適切に伝播されていました。しかし、これらの内部関数が返す具象型(例: *net.TCPConn)のnilポインタが、そのままインターフェース型(例: net.Conn)の戻り値に代入されると、前述のGoインターフェースの特性により、インターフェース型変数自体はnilではないと評価されてしまう問題がありました。

この修正では、以下のパターンが導入されています。

  1. 戻り値の明示的な宣言:

    • func dialSingle(...) (c Conn, err error) のように、戻り値の変数cerrを関数シグネチャで明示的に宣言します。
    • これにより、関数内でcerrに値を代入する際に、returnステートメントで明示的に指定する必要がなくなります(裸のreturnを使用できる)。
  2. エラーチェックと早期リターン:

    • 内部関数呼び出しの結果を一時変数(例: c, l, lp)とerrに受け取ります。
    • if err != nilでエラーをチェックします。
    • エラーが存在する場合、return nil, errのように、インターフェース型変数には明示的にnilを返し、エラーを伝播させます。
    • このreturn nil, errが重要で、これによりインターフェース型変数の(type, value)ペアの両方がnilとなり、インターフェース自体がnilと評価されるようになります。
  3. 成功時の戻り値:

    • エラーがない場合、return c, nil(または裸のreturn)で、正常に作成された接続やリスナーを返します。

この修正は、Goのインターフェースのセマンティクスを深く理解し、その特性を考慮した堅牢なエラーハンドリングパターンを適用することで、ネットワークパッケージの信頼性と使いやすさを向上させています。特に、netパッケージのような低レベルで頻繁に使用されるライブラリにおいては、このような細かな挙動の一貫性が非常に重要となります。

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

このコミットのコアとなる変更は、src/pkg/net/dial.go内のdialSingleListenListenPacket関数の戻り値の処理とエラーハンドリングロジックです。

src/pkg/net/dial.go

dialSingle関数の変更

--- a/src/pkg/net/dial.go
+++ b/src/pkg/net/dial.go
@@ -215,26 +215,30 @@ func dialMulti(net, addr string, la Addr, ras addrList, deadline time.Time) (Con
 
 // dialSingle attempts to establish and returns a single connection to
 // the destination address.
-func dialSingle(net, addr string, la, ra Addr, deadline time.Time) (Conn, error) {
+func dialSingle(net, addr string, la, ra Addr, deadline time.Time) (c Conn, err error) {
  	if la != nil && la.Network() != ra.Network() {
  		return nil, &OpError{Op: "dial", Net: net, Addr: ra, Err: errors.New("mismatched local address type " + la.Network())}\
  	}
  	switch ra := ra.(type) {
  	case *TCPAddr:
  		la, _ := la.(*TCPAddr)
- 		return dialTCP(net, la, ra, deadline)
+ 		c, err = dialTCP(net, la, ra, deadline)
  	case *UDPAddr:
  		la, _ := la.(*UDPAddr)
- 		return dialUDP(net, la, ra, deadline)
+ 		c, err = dialUDP(net, la, ra, deadline)
  	case *IPAddr:
  		la, _ := la.(*IPAddr)
- 		return dialIP(net, la, ra, deadline)
+ 		c, err = dialIP(net, la, ra, deadline)
  	case *UnixAddr:
  		la, _ := la.(*UnixAddr)
- 		return dialUnix(net, la, ra, deadline)
+ 		c, err = dialUnix(net, la, ra, deadline)
  	default:
  		return nil, &OpError{Op: "dial", Net: net, Addr: ra, Err: &AddrError{Err: "unexpected address type", Addr: addr}}\
  	}
+ 	if err != nil {
+ 		return nil, err // c is non-nil interface containing nil pointer
+ 	}
+ 	return c, nil
  }

Listen関数の変更

--- a/src/pkg/net/dial.go
+++ b/src/pkg/net/dial.go
@@ -246,14 +250,19 @@ func Listen(net, laddr string) (Listener, error) {
  	if err != nil {
  		return nil, &OpError{Op: "listen", Net: net, Addr: nil, Err: err}\
  	}
+ 	var l Listener
  	switch la := la.toAddr().(type) {
  	case *TCPAddr:
- 		return ListenTCP(net, la)
+ 		l, err = ListenTCP(net, la)
  	case *UnixAddr:
- 		return ListenUnix(net, la)
+ 		l, err = ListenUnix(net, la)
  	default:
  		return nil, &OpError{Op: "listen", Net: net, Addr: la, Err: &AddrError{Err: "unexpected address type", Addr: laddr}}\
  	}
+ 	if err != nil {
+ 		return nil, err // l is non-nil interface containing nil pointer
+ 	}
+ 	return l, nil
  }

ListenPacket関数の変更

--- a/src/pkg/net/dial.go
+++ b/src/pkg/net/dial.go
@@ -265,14 +274,19 @@ func ListenPacket(net, laddr string) (PacketConn, error) {
  	if err != nil {
  		return nil, &OpError{Op: "listen", Net: net, Addr: nil, Err: err}\
  	}
+ 	var l PacketConn
  	switch la := la.toAddr().(type) {
  	case *UDPAddr:
- 		return ListenUDP(net, la)
+ 		l, err = ListenUDP(net, la)
  	case *IPAddr:
- 		return ListenIP(net, la)
+ 		l, err = ListenIP(net, la)
  	case *UnixAddr:
- 		return ListenUnixgram(net, la)
+ 		l, err = ListenUnixgram(net, la)
  	default:
  		return nil, &OpError{Op: "listen", Net: net, Addr: la, Err: &AddrError{Err: "unexpected address type", Addr: laddr}}\
  	}
+ 	if err != nil {
+ 		return nil, err // l is non-nil interface containing nil pointer
+ 	}
+ 	return l, nil
  }

src/pkg/net/net_test.go

TestErrorNilテスト関数の追加

--- a/src/pkg/net/net_test.go
+++ b/src/pkg/net/net_test.go
@@ -218,3 +218,41 @@ func TestTCPClose(t *testing.T) {\
  		t.Fatal(err)\
  	}\
  }\
+\
+func TestErrorNil(t *testing.T) {\
+\tc, err := Dial("tcp", "127.0.0.1:65535")\
+\tif err == nil {\
+\t\tt.Fatal("Dial 127.0.0.1:65535 succeeded")\
+\t}\
+\tif c != nil {\
+\t\tt.Fatalf("Dial returned non-nil interface %T(%v) with err != nil", c, c)\
+\t}\
+\
+\t// Make Listen fail by relistening on the same address.\
+\tl, err := Listen("tcp", "127.0.0.1:0")\
+\tif err != nil {\
+\t\tt.Fatal("Listen 127.0.0.1:0: %v", err)\
+\t}\
+\tdefer l.Close()\
+\tl1, err := Listen("tcp", l.Addr().String())\
+\tif err == nil {\
+\t\tt.Fatal("second Listen %v: %v", l.Addr(), err)\
+\t}\
+\tif l1 != nil {\
+\t\tt.Fatalf("Listen returned non-nil interface %T(%v) with err != nil", l1, l1)\
+\t}\
+\
+\t// Make ListenPacket fail by relistening on the same address.\
+\tlp, err := ListenPacket("udp", "127.0.0.1:0")\
+\tif err != nil {\
+\t\tt.Fatal("Listen 127.0.0.1:0: %v", err)\
+\t}\
+\tdefer lp.Close()\
+\tlp1, err := ListenPacket("udp", lp.LocalAddr().String())\
+\tif err == nil {\
+\t\tt.Fatal("second Listen %v: %v", lp.LocalAddr(), err)\
+\t}\
+\tif lp1 != nil {\
+\t\tt.Fatalf("ListenPacket returned non-nil interface %T(%v) with err != nil", lp1, lp1)\
+\t}\
+}\

コアとなるコードの解説

変更の核心は、dialSingleListenListenPacket関数におけるエラー発生時の戻り値の処理方法です。

変更前: これらの関数は、内部で具体的な接続(例: dialTCP)やリスナー(例: ListenTCP)を作成する関数を呼び出し、その戻り値を直接returnしていました。 例えば、dialSingleのTCPの場合: return dialTCP(net, la, ra, deadline) もしdialTCPがエラーを返した場合、dialTCP(*TCPConn)(nil), errのような値を返します。この(*TCPConn)(nil)Connインターフェースに代入されると、インターフェースは(type: *net.TCPConn, value: nil)という状態になり、Conn != nilと評価されてしまう問題がありました。

変更後:

  1. 戻り値の明示的な宣言: func dialSingle(...) (c Conn, err error) のように、戻り値の変数cerrが関数シグネチャで宣言されています。これにより、関数本体内でcerrに値を代入し、最後に裸のreturnを使用できるようになります。

  2. 内部関数の呼び出しとエラーのキャプチャ: c, err = dialTCP(net, la, ra, deadline) のように、内部関数の結果を明示的にcerrに代入します。

  3. エラーチェックと安全なnil返却: if err != nil { return nil, err } この部分が最も重要です。内部関数がエラーを返した場合(err != nil)、dialSingle関数は明示的にnilとエラーを返します。このnilは、インターフェース型Connのゼロ値であり、これによりConnインターフェースの(type, value)ペアの両方がnilとなり、インターフェース自体がnilと評価されることが保証されます。コメントにある// c is non-nil interface containing nil pointerは、この修正がまさにその問題(非nilインターフェースにnilポインタが含まれる問題)を解決していることを示唆しています。

  4. 成功時の戻り値: return c, nil エラーがなければ、正常に作成されたc(接続)とnilエラーを返します。

同様の修正がListenListenPacket関数にも適用されています。

TestErrorNilの追加: このテストケースは、修正が正しく機能していることを検証するために追加されました。

  • Dial("tcp", "127.0.0.1:65535")のように、存在しないアドレスへの接続を試み、Dialがエラーを返すことを確認します。
  • さらに重要なのは、if c != nilのチェックです。エラーが返されたにもかかわらずcが非nilであればテストは失敗し、修正が正しく機能していないことを示します。
  • ListenListenPacketについても、既にアドレスが使用されているポートで再度リスナーを作成しようとすることでエラーを発生させ、同様にインターフェースがnilであることを検証しています。

このコミットは、Goのインターフェースの微妙な挙動に起因する潜在的なバグを修正し、Goの慣習に沿った堅牢なエラーハンドリングを実現することで、netパッケージの信頼性を高めています。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分 (src/pkg/net/dial.go, src/pkg/net/net_test.go)
  • Go言語のインターフェースの挙動に関する一般的な知識
  • Go言語のエラーハンドリングの慣習に関する一般的な知識
  • Go言語のnetパッケージのAPIドキュメント
  • Go言語のテストコードの読み方