[インデックス 14713] ファイルの概要
このコミットは、Go言語の標準ライブラリであるnet
パッケージ内のタイムアウトテストに関する修正です。具体的には、timeout_test.go
ファイルにおいて、Accept
、Read
、Write
操作のタイムアウトテストが、エラーの型アサーションの誤りにより、期待通りに動作しない可能性があった問題を修正しています。
コミット
net: fix timeout tests
R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/7003049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9622f5032e67561c822374714aceb779389c3c00
元コミット内容
commit 9622f5032e67561c822374714aceb779389c3c00
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Sat Dec 22 14:56:02 2012 +0900
net: fix timeout tests
R=golang-dev, minux.ma
CC=golang-dev
https://golang.org/cl/7003049
---
src/pkg/net/timeout_test.go | 33 +++++++++++++++++++++++++++------
1 file changed, 27 insertions(+), 6 deletions(-)
diff --git a/src/pkg/net/timeout_test.go b/src/pkg/net/timeout_test.go
index 21223cc74a..cda2fd73c6 100644
--- a/src/pkg/net/timeout_test.go
+++ b/src/pkg/net/timeout_test.go
@@ -60,8 +60,15 @@ func TestAcceptTimeout(t *testing.T) {
default:
}\n \tln.Close()\n-\tif err := <-errc; err.(*OpError).Err != errClosing {\n-\t\tt.Fatalf(\"Accept: expected err %v, got %v\", errClosing, err.(*OpError).Err)\n+\tswitch nerr := <-errc; err := nerr.(type) {\n+\tcase *OpError:\n+\t\tif err.Err != errClosing {\n+\t\t\tt.Fatalf(\"Accept: expected err %v, got %v\", errClosing, err)\n+\t\t}\n+\tdefault:\n+\t\tif err != errClosing {\n+\t\t\tt.Fatalf(\"Accept: expected err %v, got %v\", errClosing, err)\n+\t\t}\n \t}\n }\n \n@@ -109,8 +116,15 @@ func TestReadTimeout(t *testify.T) {\n \tdefault:\n \t}\n \tc.Close()\n-\tif err := <-errc; err.(*OpError).Err != errClosing {\n-\t\tt.Fatalf(\"Read: expected err %v, got %v\", errClosing, err.(*OpError).Err)\n+\tswitch nerr := <-errc; err := nerr.(type) {\n+\tcase *OpError:\n+\t\tif err.Err != errClosing {\n+\t\t\tt.Fatalf(\"Read: expected err %v, got %v\", errClosing, err)\n+\t\t}\n+\tdefault:\n+\t\tif err != errClosing {\n+\t\t\tt.Fatalf(\"Read: expected err %v, got %v\", errClosing, err)\n+\t\t}\n \t}\n }\n \n@@ -164,8 +178,15 @@ func TestWriteTimeout(t *testify.T) {\n \tdefault:\n \t}\n \tc.Close()\n-\tif err := <-errc; err.(*OpError).Err != errClosing {\n-\t\tt.Fatalf(\"Write: expected err %v, got %v\", errClosing, err.(*OpError).Err)\n+\tswitch nerr := <-errc; err := nerr.(type) {\n+\tcase *OpError:\n+\t\tif err.Err != errClosing {\n+\t\t\tt.Fatalf(\"Write: expected err %v, got %v\", errClosing, err)\n+\t\t}\n+\tdefault:\n+\t\tif err != errClosing {\n+\t\t\tt.Fatalf(\"Write: expected err %v, got %v\", errClosing, err)\n+\t\t}\n \t}\n }\n \n```
## 変更の背景
このコミットの背景には、Go言語の`net`パッケージにおけるネットワーク操作のタイムアウトテストが、特定のエラーシナリオで正しくエラーを捕捉できていなかった問題があります。
Goのネットワーク操作では、タイムアウトや接続切断などのエラーが発生した場合、`net.OpError`という構造体でラップされたエラーが返されることがあります。しかし、常に`net.OpError`が返されるわけではなく、場合によっては基底のエラー型(例えば、`io.EOF`や`syscall.Errno`など)が直接返されることもあります。
元のテストコードでは、エラーが常に`*net.OpError`型であると仮定し、型アサーション`err.(*OpError).Err`を使用して内部のエラーを取り出していました。このアプローチは、エラーが実際に`*net.OpError`型である場合には問題ありませんが、そうでない場合にはパニック(ランタイムエラー)を引き起こすか、あるいは意図しないエラー比較を行ってしまう可能性がありました。
特に、テストの終了処理として`Close()`が呼ばれた際に発生するエラー(`errClosing`)が、`*net.OpError`としてラップされずに直接返されるケースが存在したと考えられます。この場合、元のコードの型アサーションは失敗し、テストが不安定になったり、誤った結果を報告したりする原因となっていました。
このコミットは、このようなエラーハンドリングの不備を修正し、タイムアウトテストが様々なエラーの返却パターンに対応できるようにすることで、テストの堅牢性と正確性を向上させることを目的としています。
## 前提知識の解説
このコミットを理解するためには、以下のGo言語の概念と`net`パッケージの知識が必要です。
1. **Go言語のエラーハンドリング**:
Go言語では、エラーは`error`インターフェースを実装する型として扱われます。関数は通常、最後の戻り値として`error`型を返します。エラーが発生しなかった場合は`nil`を返します。
```go
type error interface {
Error() string
}
```
エラーの比較は、通常、`errors.Is`や`errors.As`関数(Go 1.13以降)を使用するか、直接エラー値を比較することで行われます。
2. **型アサーション (Type Assertion)**:
Go言語では、インターフェース型の変数が基底の具体的な型を持っているかどうかを確認し、その具体的な型の値を取り出すために型アサーションを使用します。
構文は`value.(Type)`です。例えば、`err.(MyErrorType)`は`err`が`MyErrorType`型であればその値を返し、そうでなければパニックを引き起こします。
安全な型アサーションとして、`value, ok := value.(Type)`という形式もあります。この場合、`ok`はアサーションが成功したかどうかを示すブール値になります。
3. **`net.OpError`**:
`net`パッケージは、ネットワーク操作中に発生したエラーを`OpError`構造体でラップして返すことがあります。`OpError`は、どの操作(Op)、どのネットワークタイプ(Net)、どのアドレス(Addr)で、どのような基底のエラー(Err)が発生したかという情報を含んでいます。
```go
type OpError struct {
Op string // "read", "write", "dial", "listen", etc.
Net string // "tcp", "udp", "ip4", "ip6", etc.
Addr Addr // address of network end that caused the error
Err error // the error that occurred
}
```
`OpError`は`error`インターフェースを実装しており、`Error()`メソッドは詳細なエラーメッセージを返します。
4. **`select`ステートメントとチャネル**:
Goの`select`ステートメントは、複数の通信操作(チャネルの送受信)を待機し、準備ができた最初の操作を実行するために使用されます。このコミットのテストコードでは、ゴルーチンで実行されるネットワーク操作からのエラーをチャネル(`errc`)で受け取るために`select`が使われています。
5. **Goのテスト (`testing`パッケージ)**:
Goの標準ライブラリには、テストを記述するための`testing`パッケージが含まれています。
* `func TestXxx(t *testing.T)`: テスト関数は`Test`で始まり、`*testing.T`型の引数を取ります。
* `t.Fatalf(...)`: テストを失敗させ、メッセージを出力してテスト関数を終了します。
## 技術的詳細
このコミットの技術的詳細は、Go言語におけるエラーハンドリングのベストプラクティスと、`net`パッケージが返すエラーの多様性への対応に集約されます。
元のコードでは、`Accept`、`Read`、`Write`の各タイムアウトテストにおいて、ネットワーク操作から返されたエラーを`errc`チャネルから受け取った後、以下のような形式でエラーをチェックしていました。
```go
if err := <-errc; err.(*OpError).Err != errClosing {
t.Fatalf("Accept: expected err %v, got %v", errClosing, err.(*OpError).Err)
}
このコードの問題点は、err.(*OpError)
という型アサーションが、err
が常に*net.OpError
型であることを前提としている点です。もしerr
が*net.OpError
型でなかった場合、この型アサーションはランタイムパニック(panic: interface conversion: interface {} is not *net.OpError: missing method Err
のようなエラー)を引き起こします。
特に、ネットワーク接続が閉じられた際に発生するエラー(errClosing
)は、必ずしも*net.OpError
としてラップされて返されるとは限りません。例えば、net.Listener
のClose()
メソッドが呼ばれた際に、Accept()
がブロックされている場合、Accept()
は直接net.ErrClosed
(またはその基底となるsyscall.EINVAL
など)を返すことがあります。これらのエラーは*net.OpError
ではないため、元のコードではテストが失敗するか、パニックが発生していました。
このコミットでは、この問題を解決するために、エラーの型をより柔軟にチェックするswitch
ステートメント(型スイッチ)を導入しています。
switch nerr := <-errc; err := nerr.(type) {
case *OpError:
if err.Err != errClosing {
t.Fatalf("Accept: expected err %v, got %v", errClosing, err)
}
default:
if err != errClosing {
t.Fatalf("Accept: expected err %v, got %v", errClosing, err)
}
}
この変更により、以下の挙動が実現されます。
-
case *OpError:
: 受け取ったエラーnerr
が*net.OpError
型である場合、その内部のエラーerr.Err
を取り出してerrClosing
と比較します。これは、OpError
でラップされたエラーに対する従来のチェック方法を維持します。 -
default:
: 受け取ったエラーnerr
が*net.OpError
型ではない場合(例えば、直接net.ErrClosed
や他の基底エラーが返された場合)、nerr
自体がerr
として扱われ、直接errClosing
と比較されます。これにより、*net.OpError
でラップされていないエラーも適切に処理できるようになります。
この修正は、Goのエラーハンドリングにおける「エラーは値である」という哲学をより忠実に反映しています。エラーの具体的な型を厳密に仮定するのではなく、複数の可能性を考慮してエラーを処理することで、テストの堅牢性が大幅に向上しています。
コアとなるコードの変更箇所
src/pkg/net/timeout_test.go
ファイルにおいて、TestAcceptTimeout
, TestReadTimeout
, TestWriteTimeout
の各関数内のエラーチェックロジックが変更されています。
--- a/src/pkg/net/timeout_test.go
+++ b/src/pkg/net/timeout_test.go
@@ -60,8 +60,15 @@ func TestAcceptTimeout(t *testing.T) {
default:
}\n \tln.Close()\n-\tif err := <-errc; err.(*OpError).Err != errClosing {\n-\t\tt.Fatalf(\"Accept: expected err %v, got %v\", errClosing, err.(*OpError).Err)\n+\tswitch nerr := <-errc; err := nerr.(type) {\n+\tcase *OpError:\n+\t\tif err.Err != errClosing {\n+\t\t\tt.Fatalf(\"Accept: expected err %v, got %v\", errClosing, err)\n+\t\t}\n+\tdefault:\n+\t\tif err != errClosing {\n+\t\t\tt.Fatalf(\"Accept: expected err %v, got %v\", errClosing, err)\n+\t\t}\n \t}\n }\
\n@@ -109,8 +116,15 @@ func TestReadTimeout(t *testing.T) {
default:
}\n \tc.Close()\n-\tif err := <-errc; err.(*OpError).Err != errClosing {\n-\t\tt.Fatalf(\"Read: expected err %v, got %v\", errClosing, err.(*OpError).Err)\n+\tswitch nerr := <-errc; err := nerr.(type) {\n+\tcase *OpError:\n+\t\tif err.Err != errClosing {\n+\t\t\tt.Fatalf(\"Read: expected err %v, got %v\", errClosing, err)\n+\t\t}\n+\tdefault:\n+\t\tif err != errClosing {\n+\t\t\tt.Fatalf(\"Read: expected err %v, got %v\", errClosing, err)\n+\t\t}\n \t}\n }\
\n@@ -164,8 +178,15 @@ func TestWriteTimeout(t *testing.T) {
default:
}\n \tc.Close()\n-\tif err := <-errc; err.(*OpError).Err != errClosing {\n-\t\tt.Fatalf(\"Write: expected err %v, got %v\", errClosing, err.(*OpError).Err)\n+\tswitch nerr := <-errc; err := nerr.(type) {\n+\tcase *OpError:\n+\t\tif err.Err != errClosing {\n+\t\t\tt.Fatalf(\"Write: expected err %v, got %v\", errClosing, err)\n+\t\t}\n+\tdefault:\n+\t\tif err != errClosing {\n+\t\t\tt.Fatalf(\"Write: expected err %v, got %v\", errClosing, err)\n+\t\t}\n \t}\n }\
\n```
## コアとなるコードの解説
変更の核心は、エラーチャネル`errc`から受け取ったエラー`nerr`の処理方法を、`if`文と直接的な型アサーションから、より柔軟な`switch`ステートメント(型スイッチ)へと変更した点です。
**変更前:**
```go
if err := <-errc; err.(*OpError).Err != errClosing {
t.Fatalf("... expected err %v, got %v", errClosing, err.(*OpError).Err)
}
このコードは、errc
から受け取ったエラーが必ず*net.OpError
型であると仮定しています。そして、そのOpError
の内部エラーErr
フィールドを取り出してerrClosing
と比較しています。もしerr
が*net.OpError
型でなかった場合、err.(*OpError)
の部分でパニックが発生し、テストがクラッシュする可能性がありました。
変更後:
switch nerr := <-errc; err := nerr.(type) {
case *OpError:
if err.Err != errClosing {
t.Fatalf("... expected err %v, got %v", errClosing, err)
}
default:
if err != errClosing {
t.Fatalf("... expected err %v, got %v", errClosing, err)
}
}
この新しいコードは、以下の2つのケースを考慮しています。
-
case *OpError:
:nerr
が*net.OpError
型である場合、err
変数には*net.OpError
型の値が代入されます。この場合、以前と同様にerr.Err
(OpError
の内部エラー)を取り出してerrClosing
と比較します。これにより、OpError
でラップされたエラーに対する既存のロジックが維持されます。 -
default:
:nerr
が*net.OpError
型ではない場合(例えば、net.ErrClosed
のような直接的なエラー値が返された場合)、err
変数にはnerr
の元の型(error
インターフェースを実装する任意の型)の値が代入されます。この場合、err
自体をerrClosing
と比較します。これにより、OpError
でラップされていないエラーも適切に処理できるようになります。
この修正により、テストはネットワーク操作が返す可能性のある様々なエラーの型に対応できるようになり、より堅牢で正確なタイムアウトテストが実現されました。特に、Close()
操作によって発生するエラーが*net.OpError
としてラップされないケースを適切にハンドリングできるようになった点が重要です。
関連リンク
- Go CL 7003049: https://golang.org/cl/7003049
参考にした情報源リンク
- Go言語の
net
パッケージドキュメント: https://pkg.go.dev/net - Go言語のエラーハンドリングに関する公式ブログ記事やドキュメント (例: "Error Handling and Go"): https://go.dev/blog/error-handling-and-go
- Go言語の型アサーションに関するドキュメント: https://go.dev/tour/methods/15
- Go言語の
testing
パッケージドキュメント: https://pkg.go.dev/testing - Go言語の
select
ステートメントに関するドキュメント: https://go.dev/tour/concurrency/5 - Go言語におけるエラーの比較 (
errors.Is
,errors.As
): https://pkg.go.dev/errors - Go言語の
net.OpError
の定義と使用例に関する情報 (Goのソースコードや関連する議論): https://github.com/golang/go/blob/master/src/net/error.go (Goのソースコード) - Go言語の
net.ErrClosed
に関する情報: https://pkg.go.dev/net#pkg-variables (Goのドキュメント) - Go言語の
syscall
パッケージに関する情報 (例:syscall.EINVAL
): https://pkg.go.dev/syscall