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

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

このコミットは、Go言語の標準ライブラリである net パッケージ内の Dial 関数におけるエラー情報の重複を解消することを目的としています。具体的には、Dial 関数が返すエラーが OpError 型である場合に、そのエラーがさらに別の OpError でラップされてしまうという冗長な挙動を修正し、エラーメッセージが「dial ... dial ...」のように二重に表示されるのを防ぎます。

コミット

  • コミットハッシュ: 565793996c2b646dfa31c6660d79a57a7ae8312e
  • Author: Andrey Mirtchovski mirtchovski@gmail.com
  • Date: Tue Oct 18 14:51:40 2011 -0400

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

https://github.com/golang/go/commit/565793996c2b646dfa31c6660d79a57a7ae8312e

元コミット内容

net: remove duplicate error information in Dial

R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5293045

変更の背景

Go言語の net パッケージには、ネットワーク接続を確立するための Dial 関数が存在します。この関数は、接続に失敗した場合にエラーを返します。Goのエラーハンドリングの慣習として、エラーは通常、そのエラーが発生した操作に関する追加情報(例えば、操作の種類、ネットワークアドレスなど)を含む OpError 型でラップされることがあります。

しかし、このコミットが修正する前の Dial 関数では、エラーが発生した際に、既に OpError 型であるエラーをさらに別の OpError でラップしてしまうという問題がありました。これにより、エラーメッセージが「dial tcp 127.0.0.1:80: dial unknown network tcp」のように、操作の種類(dial)が二重に表示されるなど、冗長で分かりにくいエラー情報がユーザーに提示されていました。

この重複は、エラーの根本原因を特定する際に混乱を招く可能性があり、また、エラーメッセージの解析を困難にするため、修正が必要とされました。このコミットは、この冗長なエラー情報の生成を防ぎ、よりクリーンで正確なエラーメッセージを提供することを目的としています。

前提知識の解説

Go言語のエラーハンドリング

Go言語では、エラーは組み込みの error インターフェースによって表現されます。関数がエラーを返す可能性がある場合、通常は最後の戻り値として error 型を返します。呼び出し元は、返されたエラーが nil でない場合にエラーが発生したと判断し、適切に処理します。

func someFunction() (resultType, error) {
    // ... 処理 ...
    if someCondition {
        return zeroValue, errors.New("something went wrong")
    }
    return actualResult, nil
}

net.OpError

net パッケージでは、ネットワーク操作中に発生するエラーをより詳細に表現するために OpError という構造体が定義されています。OpErrorerror インターフェースを実装しており、以下のフィールドを持ちます。

  • Op (string): 実行しようとした操作の種類(例: "dial", "read", "write")。
  • Net (string): ネットワークの種類(例: "tcp", "udp")。
  • Addr (Addr): ネットワークアドレス。
  • Err (error): 根本的なエラー。

OpErrorError() メソッドは、これらのフィールドを組み合わせて、より情報量の多いエラーメッセージを生成します。例えば、OpError{Op: "dial", Net: "tcp", Addr: "127.0.0.1:80", Err: someUnderlyingError} は、「dial tcp 127.0.0.1:80: some underlying error」のようなメッセージを生成します。

net.Dial 関数

net.Dial 関数は、指定されたネットワークプロトコル(例: "tcp", "udp")とアドレスを使用して、ネットワーク接続を確立します。

func Dial(net, addr string) (Conn, error)

この関数は、成功した場合は net.Conn インターフェースを実装する接続オブジェクトを、失敗した場合はエラーを返します。

os.Error (Go 1.0以前)

このコミットが作成された2011年時点では、Go言語のエラーインターフェースは os.Error という名前でした。Go 1.0のリリースに伴い、これは error に名称変更され、組み込み型となりました。コミットのコードスニペットでは os.Error が使用されていますが、現代のGoコードでは error と読み替える必要があります。

技術的詳細

このコミットの技術的な核心は、net.Dial 関数内でエラーが生成されるロジックの変更にあります。

変更前のコードでは、Dial 関数は内部で様々なネットワーク操作を行い、それぞれがエラーを返す可能性があります。これらの内部エラーは、最終的に Dial 関数から返される前に、OpError でラップされることがありました。

特に問題となっていたのは、Dial 関数が内部で DialIP などの他の Dial 系関数を呼び出す場合です。これらの内部呼び出しもまた OpError を返す可能性がありました。変更前のロジックでは、Dial 関数が受け取ったエラーが既に OpError であったとしても、そのエラーをさらに新しい OpError でラップしていました。

例えば、UnknownNetworkError(net) が既に OpError を返していた場合、以下の行でその OpError がさらにラップされていました。

// 変更前:
// default:
//     err = UnknownNetworkError(net) // err が既に OpError の可能性がある
// }
// if err != nil {
//     return nil, &OpError{"dial", net + " " + addr, nil, err} // ここで二重にラップされる
// }

この二重ラップにより、エラーメッセージは「dial <network> <address>: dial <original_error_message>」のような形式になり、dial という操作が重複して表示されていました。

このコミットでは、この問題を解決するために以下の2つの変更が行われました。

  1. UnknownNetworkError の即時ラップ: Dial 関数内の default ケースで UnknownNetworkError(net) が返される際に、その場で OpError でラップするように変更されました。これにより、UnknownNetworkError が返すエラーが何であれ、一度だけ OpError でラップされることが保証されます。

    // 変更後:
    // default:
    //     err = &OpError{"dial", net + " " + addr, nil, UnknownNetworkError(net)} // ここで一度だけラップ
    // }
    
  2. 最終的なエラーの直接返却: Dial 関数の最後にエラーを返す部分で、既に err 変数に格納されているエラーをそのまま返すように変更されました。これにより、err が既に OpError であった場合でも、新たな OpError でラップされることなく、既存の OpError が直接返されるようになります。

    // 変更後:
    // if err != nil {
    //     return nil, err // err をそのまま返す
    // }
    

これらの変更により、Dial 関数から返されるエラーは、常に一度だけ OpError で適切にラップされるようになり、エラー情報の重複が解消されました。

また、この変更の正しさを検証するために、net_test.go に新しいテストケースが追加されました。このテストは、エラーメッセージ内に「dial ... dial ...」という重複パターンが含まれていないことを確認することで、修正が意図通りに機能していることを保証します。正規表現 dial (.*) dial (.*) を使用して、この重複パターンを検出します。

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

src/pkg/net/dial.go

--- a/src/pkg/net/dial.go
+++ b/src/pkg/net/dial.go
@@ -59,10 +59,10 @@ func Dial(net, addr string) (c Conn, err os.Error) {
 	case *IPAddr:
 		c, err = DialIP(net, nil, ra)
 	default:
-		err = UnknownNetworkError(net)
+		err = &OpError{"dial", net + " " + addr, nil, UnknownNetworkError(net)}
 	}
 	if err != nil {
-		return nil, &OpError{"dial", net + " " + addr, nil, err}
+		return nil, err
 	}
 	return
 }

src/pkg/net/net_test.go

--- a/src/pkg/net/net_test.go
+++ b/src/pkg/net/net_test.go
@@ -62,6 +62,8 @@ var dialErrorTests = []DialErrorTest{
 	},
 }

+var duplicateErrorPattern = `dial (.*) dial (.*)`
+
 func TestDialError(t *testing.T) {
 	if !*runErrorTest {
 		t.Logf("test disabled; use --run_error_test to enable")
@@ -81,6 +83,10 @@ func TestDialError(t *testing.T) {\n 		if !match {\n 			t.Errorf("#%d: %q, want match for %#q\", i, s, tt.Pattern)\n 		}\n+		match, _ = regexp.MatchString(duplicateErrorPattern, s)\n+		if match {\n+			t.Errorf("#%d: %q, duplicate error return from Dial\", i, s)\n+		}\n 	}\n }\n \n```

コアとなるコードの解説

src/pkg/net/dial.go の変更

  1. default ケース内の変更: 変更前: err = UnknownNetworkError(net) 変更後: err = &OpError{"dial", net + " " + addr, nil, UnknownNetworkError(net)}

    この変更は、Dial 関数が未知のネットワークタイプを扱おうとした場合に発生するエラーの生成方法を修正します。変更前は、UnknownNetworkError(net) が直接 err に代入されていました。もし UnknownNetworkError 自体が OpError を返す実装であった場合、後続の if err != nil ブロックでさらに OpError でラップされる可能性がありました。 変更後は、UnknownNetworkError(net) が返すエラーを、その場で OpError でラップしています。これにより、Dial 操作に関する情報("dial", net + " " + addr)が一度だけ、かつ適切なタイミングでエラーに付加されることが保証されます。

  2. 最終的なエラー返却箇所の変更: 変更前: return nil, &OpError{"dial", net + " " + addr, nil, err} 変更後: return nil, err

    この変更は、Dial 関数が最終的にエラーを返す際の挙動を修正します。変更前は、err 変数に格納されているエラーが何であれ、常に新しい OpError でラップして返していました。これは、err が既に OpError であった場合に、エラー情報の重複を引き起こす原因となっていました。 変更後は、err 変数に格納されているエラーをそのまま返します。これにより、エラーが既に OpError で適切にフォーマットされている場合は、その OpError が直接返され、冗長なラップが回避されます。

これらの変更により、Dial 関数は、エラーが発生した場合に常に一貫性のある、重複のない OpError を返すようになります。

src/pkg/net/net_test.go の変更

  1. duplicateErrorPattern の追加: var duplicateErrorPattern = dial (.) dial (.)``

    この正規表現は、エラーメッセージ内に「dial ... dial ...」という形式の重複パターンが存在するかどうかを検出するために定義されました。() はキャプチャグループを示し、(.*) は任意の文字が0回以上繰り返されるパターンにマッチします。これにより、「dial」という単語が2回連続して現れ、その間に何らかの文字列がある場合にマッチします。

  2. TestDialError 関数内のテストロジックの追加:

    match, _ = regexp.MatchString(duplicateErrorPattern, s)
    if match {
        t.Errorf("#%d: %q, duplicate error return from Dial", i, s)
    }
    

    TestDialError 関数は、dialErrorTests というテストケースのスライスを反復処理し、各テストケースで Dial 関数を呼び出してエラーを検証します。 追加されたコードは、regexp.MatchString を使用して、Dial 関数が返したエラーメッセージ sduplicateErrorPattern にマッチするかどうかを確認します。 もしマッチした場合(つまり、エラーメッセージに「dial ... dial ...」という重複が含まれていた場合)、t.Errorf を呼び出してテストを失敗させます。これは、Dial 関数が重複したエラー情報を返していることを示し、修正が正しく機能していないことを意味します。

このテストの追加により、Dial 関数のエラー処理が正しく、重複のないエラーメッセージを生成していることが自動的に検証されるようになりました。

関連リンク

参考にした情報源リンク

  • 上記のGitHubコミットページとGo Code Review (CL)
  • Go言語の公式ドキュメント(net パッケージ、エラーハンドリングに関する情報)
  • Go言語のエラー処理に関する一般的な慣習とベストプラクティスに関する情報