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

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

このコミットは、Go言語の標準ライブラリであるnetパッケージ内のタイムアウトテストに関する修正です。具体的には、timeout_test.goファイルにおいて、AcceptReadWrite操作のタイムアウトテストが、エラーの型アサーションの誤りにより、期待通りに動作しない可能性があった問題を修正しています。

コミット

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.ListenerClose()メソッドが呼ばれた際に、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)
    }
}

この変更により、以下の挙動が実現されます。

  1. case *OpError:: 受け取ったエラーnerr*net.OpError型である場合、その内部のエラーerr.Errを取り出してerrClosingと比較します。これは、OpErrorでラップされたエラーに対する従来のチェック方法を維持します。

  2. 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つのケースを考慮しています。

  1. case *OpError:: nerr*net.OpError型である場合、err変数には*net.OpError型の値が代入されます。この場合、以前と同様にerr.ErrOpErrorの内部エラー)を取り出してerrClosingと比較します。これにより、OpErrorでラップされたエラーに対する既存のロジックが維持されます。

  2. default:: nerr*net.OpError型ではない場合(例えば、net.ErrClosedのような直接的なエラー値が返された場合)、err変数にはnerrの元の型(errorインターフェースを実装する任意の型)の値が代入されます。この場合、err自体をerrClosingと比較します。これにより、OpErrorでラップされていないエラーも適切に処理できるようになります。

この修正により、テストはネットワーク操作が返す可能性のある様々なエラーの型に対応できるようになり、より堅牢で正確なタイムアウトテストが実現されました。特に、Close()操作によって発生するエラーが*net.OpErrorとしてラップされないケースを適切にハンドリングできるようになった点が重要です。

関連リンク

参考にした情報源リンク