[インデックス 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
という構造体が定義されています。OpError
は error
インターフェースを実装しており、以下のフィールドを持ちます。
Op
(string): 実行しようとした操作の種類(例: "dial", "read", "write")。Net
(string): ネットワークの種類(例: "tcp", "udp")。Addr
(Addr): ネットワークアドレス。Err
(error): 根本的なエラー。
OpError
の Error()
メソッドは、これらのフィールドを組み合わせて、より情報量の多いエラーメッセージを生成します。例えば、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つの変更が行われました。
-
UnknownNetworkError
の即時ラップ:Dial
関数内のdefault
ケースでUnknownNetworkError(net)
が返される際に、その場でOpError
でラップするように変更されました。これにより、UnknownNetworkError
が返すエラーが何であれ、一度だけOpError
でラップされることが保証されます。// 変更後: // default: // err = &OpError{"dial", net + " " + addr, nil, UnknownNetworkError(net)} // ここで一度だけラップ // }
-
最終的なエラーの直接返却:
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
の変更
-
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
)が一度だけ、かつ適切なタイミングでエラーに付加されることが保証されます。 -
最終的なエラー返却箇所の変更: 変更前:
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
の変更
-
duplicateErrorPattern
の追加:var duplicateErrorPattern =
dial (.) dial (.)``この正規表現は、エラーメッセージ内に「dial ... dial ...」という形式の重複パターンが存在するかどうかを検出するために定義されました。
(
と)
はキャプチャグループを示し、(.*)
は任意の文字が0回以上繰り返されるパターンにマッチします。これにより、「dial」という単語が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
関数が返したエラーメッセージs
がduplicateErrorPattern
にマッチするかどうかを確認します。 もしマッチした場合(つまり、エラーメッセージに「dial ... dial ...」という重複が含まれていた場合)、t.Errorf
を呼び出してテストを失敗させます。これは、Dial
関数が重複したエラー情報を返していることを示し、修正が正しく機能していないことを意味します。
このテストの追加により、Dial
関数のエラー処理が正しく、重複のないエラーメッセージを生成していることが自動的に検証されるようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/565793996c2b646dfa31c6660d79a57a7ae8312e
- Go Code Review (CL): https://golang.org/cl/5293045
参考にした情報源リンク
- 上記のGitHubコミットページとGo Code Review (CL)
- Go言語の公式ドキュメント(
net
パッケージ、エラーハンドリングに関する情報) - Go言語のエラー処理に関する一般的な慣習とベストプラクティスに関する情報