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

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

このコミットは、Go言語の標準ライブラリである net パッケージ内のテストファイル server_test.go の変更に関するものです。具体的には、テストの失敗時の挙動を改善し、より詳細なエラーメッセージが出力されるように修正されています。これにより、テストが失敗した際に問題の原因を特定しやすくなります。

コミット

commit ea1f7b83800f769d16384c759f2e373bb492f336
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Wed Feb 13 16:17:47 2013 +1100

    net: change server_test.go so we could see failure messages
    
    R=golang-dev, dave
    CC=golang-dev
    https://golang.org/cl/7323051

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

https://github.com/golang/go/commit/ea1f7b83800f769d16384c759f2e373bb492f336

元コミット内容

net: change server_test.go so we could see failure messages

このコミットメッセージは、net パッケージの server_test.go ファイルを変更し、テスト失敗時のメッセージがより明確に表示されるようにしたことを示しています。

変更の背景

Go言語のテストフレームワークでは、テスト中にエラーが発生した場合に t.Errorft.Fatalf の2つの主要な関数を使用してエラーを報告します。

  • t.Errorf: エラーを報告しますが、テストの実行は継続します。
  • t.Fatalf: エラーを報告し、現在のテスト関数を即座に終了させます。

元の server_test.go では、ネットワーク操作に関連する多くのエラーチェックで t.Errorf が使用されていました。これは、エラーが発生してもテストが最後まで実行され、複数のエラーが報告される可能性があるという利点があります。しかし、ネットワークテストのような非同期処理や、特定の初期化ステップが失敗した場合、その後のテストロジックは意味をなさず、さらなるエラーを引き起こす可能性があります。このような場合、テストの初期段階で致命的なエラーが発生した際に、その後の無意味な処理をスキップし、根本的な原因に焦点を当てることが重要になります。

このコミットの背景には、テストが失敗した際に、その失敗がテスト全体の実行を停止させず、結果として多くの無関係なエラーメッセージが生成され、真の原因が埋もれてしまうという問題があったと考えられます。t.Fatalf を使用することで、致命的なエラーが発生した時点でテストを中断し、そのエラーメッセージを確実に確認できるようにすることが目的です。

また、done チャネルの扱いも変更されています。Goのチャネルはゴルーチン間の通信に使用されますが、テストの完了を通知するために使用されることがあります。チャネルに値を送信する (done <- 1) のではなく、チャネルを閉じる (close(done)) ことで、複数の受信側がチャネルが閉じられたことを検知し、テストの完了をより明確に通知できるようになります。defer close(done) を追加することで、関数が終了する際に確実にチャネルが閉じられるようになり、リソースリークやデッドロックのリスクを低減します。

さらに、Accept failed のログ出力が追加されたことで、サーバーがクライアントからの接続を受け入れる際に発生するエラーがテストログに記録されるようになり、デバッグ情報が増強されています。

前提知識の解説

Go言語のテスト (testing パッケージ)

Go言語には標準で testing パッケージが用意されており、ユニットテストやベンチマークテストを記述できます。テストファイルは通常、テスト対象のファイルと同じディレクトリに _test.go というサフィックスを付けて配置されます。

  • *testing.T: テスト関数に渡される構造体で、テストの実行状態を管理し、エラー報告やログ出力のためのメソッドを提供します。
  • t.Errorf(format string, args ...interface{}): テスト中にエラーが発生したことを報告します。テストは失敗としてマークされますが、テスト関数の実行は継続されます。
  • t.Fatalf(format string, args ...interface{}): テスト中に致命的なエラーが発生したことを報告します。テストは失敗としてマークされ、現在のテスト関数の実行は即座に停止します。これは、その後のテストロジックが意味をなさない場合に特に有用です。
  • t.Logf(format string, args ...interface{}): テスト中に情報をログに出力します。これはテストが失敗した場合にのみ表示されます(go test -v を使用しない限り)。デバッグ情報として利用されます。

Go言語のチャネル (Channels)

Go言語のチャネルは、ゴルーチン間で値を送受信するための通信メカニズムです。チャネルは型付けされており、特定の型の値のみを送受信できます。

  • チャネルの作成: ch := make(chan int)
  • 値の送信: ch <- value
  • 値の受信: value := <-ch
  • チャネルのクローズ: close(ch)
    • チャネルをクローズすると、それ以上値を送信できなくなります。
    • クローズされたチャネルから値を受信しようとすると、チャネルにまだ値が残っていればその値が返され、値がなければゼロ値が返されます。受信操作はブロックされません。
    • v, ok := <-ch のように2つの戻り値で受信すると、ok はチャネルがクローズされる前に値が送信された場合は true、チャネルがクローズされた後にゼロ値が返された場合は false になります。
  • defer ステートメント: defer に続く関数呼び出しは、その関数がリターンする直前に実行されます。これはリソースの解放(ファイルのクローズ、ロックの解除など)に非常に便利です。

ネットワークプログラミングの基礎

このコミットは net パッケージのテストに関するものであり、ネットワークプログラミングの基本的な概念が関連します。

  • ソケット (Socket): ネットワーク通信のエンドポイント。
  • リスナー (Listener): サーバー側で、特定のネットワークアドレスとポートで着信接続を待機するオブジェクト。
  • 接続 (Connection): クライアントとサーバー間の確立された通信パス。
  • Listen 関数: サーバー側で、指定されたネットワークとアドレスで着信接続をリッスンするためのリスナーを作成します。
  • Accept メソッド: リスナーが着信接続を受け入れ、新しい接続を返します。
  • Dial 関数: クライアント側で、指定されたネットワークとアドレスに接続を確立します。
  • Read / Write メソッド: 接続を介してデータの読み書きを行います。

技術的詳細

このコミットの技術的な変更点は大きく分けて以下の3つです。

  1. t.Errorf から t.Fatalf への変更: SplitHostPort, Dial, Write, Read, ResolveUDPAddr, ResolveUnixAddr, ListenPacket など、ネットワーク操作の初期段階や重要なステップでエラーが発生した場合に、t.Errorf ではなく t.Fatalf を使用するように変更されました。 これは、これらのエラーがテストの続行を無意味にするような致命的な問題であると判断されたためです。例えば、SplitHostPort が失敗した場合、その後のアドレス解決や接続確立は不可能であり、テストを継続してもさらなるエラーを報告するだけになります。t.Fatalf を使用することで、テストは即座に停止し、根本的なエラーメッセージが明確に表示され、デバッグが容易になります。

  2. done チャネルの扱い方の変更: runStreamConnServer 関数内で、テストの完了を通知するために使用されていた done チャネルへの値送信 (done <- 1) が、チャネルのクローズ (close(done)) に変更されました。

    • 元のコードでは、done <- 1 が複数箇所に存在し、チャネルに値を送信することで完了を通知していました。しかし、チャネルは一度クローズされるとそれ以上値を送信できません。また、複数のゴルーチンが done チャネルの完了を待っている場合、close(done) はすべての受信側に対してチャネルが閉じられたことを通知する効率的な方法です。
    • defer close(done)runStreamConnServer の冒頭に追加されました。これにより、関数がどのような経路で終了しても(正常終了、エラーによる早期リターンなど)、done チャネルが確実にクローズされることが保証されます。これは、チャネルのリークを防ぎ、テストのクリーンアップを確実に行う上で重要です。
  3. t.Logf の追加: runStreamConnServer 関数内の l.Accept() がエラーを返した場合に、t.Logf("Accept failed: %v", err) が追加されました。 Accept は、クライアントからの接続を受け入れる際にエラーを返す可能性があります。例えば、リスナーがクローズされた後や、システムリソースの枯渇などです。以前は continue run でエラーを無視していましたが、この変更により、Accept が失敗した具体的な理由がテストログに記録されるようになり、デバッグ時の情報が豊富になります。t.Logft.Errorft.Fatalf と異なり、テストが失敗した場合にのみログが表示されるため、成功時には余分な出力がありません。

これらの変更は、Goのテストにおけるエラーハンドリングのベストプラクティスに沿ったものであり、テストの信頼性とデバッグの容易性を向上させることを目的としています。

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

変更は src/pkg/net/server_test.go ファイルに集中しています。

--- a/src/pkg/net/server_test.go
+++ b/src/pkg/net/server_test.go
@@ -113,8 +113,7 @@ func TestStreamConnServer(t *testing.T) {
 		case "tcp", "tcp4", "tcp6":
 			_, port, err := SplitHostPort(taddr)
 			if err != nil {
-				t.Errorf("SplitHostPort(%q) failed: %v", taddr, err)
-				return
+				t.Fatalf("SplitHostPort(%q) failed: %v", taddr, err)
 			}
 			taddr = tt.caddr + ":" + port
 		}
@@ -169,11 +168,11 @@ func TestSeqpacketConnServer(t *testing.T) {
 }
 
 func runStreamConnServer(t *testing.T, net, laddr string, listening chan<- string, done chan<- int) {
+	defer close(done)
 	l, err := Listen(net, laddr)
 	if err != nil {
 		t.Errorf("Listen(%q, %q) failed: %v", net, laddr, err)
 		listening <- "<nil>"
-		done <- 1
 		return
 	}
 	defer l.Close()
@@ -188,13 +187,14 @@ func runStreamConnServer(t *testing.T, net, laddr string, listening chan<- strin
 			}
 			rw.Write(buf[0:n])
 		}
-		done <- 1
+		close(done)
 	}
 
 run:
 	for {
 		c, err := l.Accept()
 		if err != nil {
+			t.Logf("Accept failed: %v", err)
 			continue run
 		}
 		echodone := make(chan int)
@@ -203,14 +202,12 @@ run:
 		c.Close()
 		break run
 	}
-	done <- 1
 }
 
 func runStreamConnClient(t *testing.T, net, taddr string, isEmpty bool) {
 	c, err := Dial(net, taddr)
 	if err != nil {
-		t.Errorf("Dial(%q, %q) failed: %v", net, taddr, err)
-		return
+		t.Fatalf("Dial(%q, %q) failed: %v", net, taddr, err)
 	}
 	defer c.Close()
 	c.SetReadDeadline(time.Now().Add(1 * time.Second))
@@ -220,14 +217,12 @@ func runStreamConnClient(t *testing.T, net, taddr string, isEmpty bool) {
 	if n, err := c.Write(wb); err != nil || n != len(wb) {
 		t.Errorf("Write failed: %v, %v; want %v, <nil>\", n, err, len(wb))
 		return
+		t.Fatalf("Write failed: %v, %v; want %v, <nil>\", n, err, len(wb))
 	}
 
 	rb := make([]byte, 1024)
 	if n, err := c.Read(rb[0:]); err != nil || n != len(wb) {
-		t.Errorf("Read failed: %v, %v; want %v, <nil>\", n, err, len(wb))
-		return
+		t.Fatalf("Read failed: %v, %v; want %v, <nil>\", n, err, len(wb))
 	}
 
 	// Send explicit ending for unixpacket.
@@ -333,8 +329,7 @@ func TestDatagramPacketConnServer(t *testing.T) {
 		case "udp", "udp4", "udp6":
 			_, port, err := SplitHostPort(taddr)
 			if err != nil {
-				t.Errorf("SplitHostPort(%q) failed: %v", taddr, err)
-				return
+				t.Fatalf("SplitHostPort(%q) failed: %v", taddr, err)
 			}
 			taddr = tt.caddr + ":" + port
 			tt.caddr += ":0"
@@ -397,14 +392,12 @@ func runDatagramConnClient(t *testing.T, net, laddr, taddr string, isEmpty bool)
 	case "udp", "udp4", "udp6":
 		c, err = Dial(net, taddr)
 		if err != nil {
-			t.Errorf("Dial(%q, %q) failed: %v", net, taddr, err)
-			return
+			t.Fatalf("Dial(%q, %q) failed: %v", net, taddr, err)
 		}
 	case "unixgram":
 		c, err = DialUnix(net, &UnixAddr{laddr, net}, &UnixAddr{taddr, net})
 		if err != nil {
-			t.Errorf("DialUnix(%q, {%q, %q}) failed: %v", net, laddr, taddr, err)
-			return
+			t.Fatalf("DialUnix(%q, {%q, %q}) failed: %v", net, laddr, taddr, err)
 		}
 	}
 	defer c.Close()
@@ -415,14 +408,12 @@ func runDatagramConnClient(t *testing.T, net, laddr, taddr string, isEmpty bool)
 	if n, err := c.Write(wb[0:]); err != nil || n != len(wb) {
 		t.Errorf("Write failed: %v, %v; want %v, <nil>\", n, err, len(wb))
 		return
+		t.Fatalf("Write failed: %v, %v; want %v, <nil>\", n, err, len(wb))
 	}
 
 	rb := make([]byte, 1024)
 	if n, err := c.Read(rb[0:]); err != nil || n != len(wb) {
-		t.Errorf("Read failed: %v, %v; want %v, <nil>\", n, err, len(wb))
-		return
+		t.Fatalf("Read failed: %v, %v; want %v, <nil>\", n, err, len(wb))
 	}
 }
 
@@ -433,20 +424,17 @@ func runDatagramPacketConnClient(t *testing.T, net, laddr, taddr string, isEmpty
 	case "udp", "udp4", "udp6":
 		ra, err = ResolveUDPAddr(net, taddr)
 		if err != nil {
-			t.Errorf("ResolveUDPAddr(%q, %q) failed: %v", net, taddr, err)
-			return
+			t.Fatalf("ResolveUDPAddr(%q, %q) failed: %v", net, taddr, err)
 		}
 	case "unixgram":
 		ra, err = ResolveUnixAddr(net, taddr)
 		if err != nil {
-			t.Errorf("ResolveUxixAddr(%q, %q) failed: %v", net, taddr, err)
-			return
+			t.Fatalf("ResolveUxixAddr(%q, %q) failed: %v", net, taddr, err)
 		}
 	}
 	c, err := ListenPacket(net, laddr)
 	if err != nil {
-		t.Errorf("ListenPacket(%q, %q) faild: %v", net, laddr, err)
-		return
+		t.Fatalf("ListenPacket(%q, %q) faild: %v", net, laddr, err)
 	}
 	defer c.Close()
 	c.SetReadDeadline(time.Now().Add(1 * time.Second))
@@ -456,13 +444,11 @@ func runDatagramPacketConnClient(t *testing.T, net, laddr, taddr string, isEmpty
 	if n, err := c.WriteTo(wb[0:], ra); err != nil || n != len(wb) {
 		t.Errorf("WriteTo(%v) failed: %v, %v; want %v, <nil>\", ra, n, err, len(wb))
 		return
+		t.Fatalf("WriteTo(%v) failed: %v, %v; want %v, <nil>\", ra, n, err, len(wb))
 	}
 
 	rb := make([]byte, 1024)
 	if n, _, err := c.ReadFrom(rb[0:]); err != nil || n != len(wb) {
-		t.Errorf("ReadFrom failed: %v, %v; want %v, <nil>\", n, err, len(wb))
-		return
+		t.Fatalf("ReadFrom failed: %v, %v; want %v, <nil>\", n, err, len(wb))
 	}
 }

コアとなるコードの解説

t.Errorf から t.Fatalf への変更

TestStreamConnServer, runStreamConnClient, TestDatagramPacketConnServer, runDatagramConnClient, runDatagramPacketConnClient 関数内で、以下のような変更が行われています。

変更前:

if err != nil {
    t.Errorf("Some error message: %v", err)
    return // テスト関数を終了
}

変更後:

if err != nil {
    t.Fatalf("Some error message: %v", err) // テスト関数を即座に終了
}

この変更により、SplitHostPortDialWriteReadResolveUDPAddrResolveUnixAddrListenPacket といった重要なネットワーク操作が失敗した場合、テストは即座に停止し、そのエラーが致命的なものとして扱われます。これにより、テストの失敗原因がより明確になり、デバッグが容易になります。

done チャネルの扱い方の変更

runStreamConnServer 関数内で、done チャネルの扱いが変更されました。

変更前:

func runStreamConnServer(t *testing.T, net, laddr string, listening chan<- string, done chan<- int) {
    // ...
    if err != nil {
        // ...
        done <- 1 // チャネルに値を送信して完了を通知
        return
    }
    // ...
    for {
        // ...
        if err != nil {
            // ...
            continue run
        }
        // ...
        done <- 1 // チャネルに値を送信して完了を通知
    }
    done <- 1 // チャネルに値を送信して完了を通知
}

変更後:

func runStreamConnServer(t *testing.T, net, laddr string, listening chan<- string, done chan<- int) {
    defer close(done) // 関数終了時にチャネルをクローズ
    l, err := Listen(net, laddr)
    if err != nil {
        t.Errorf("Listen(%q, %q) failed: %v", net, laddr, err)
        listening <- "<nil>"
        // done <- 1 は削除された
        return
    }
    defer l.Close()
    // ...
    for {
        // ...
        close(done) // チャネルをクローズして完了を通知
    }
    // done <- 1 は削除された
}

defer close(done) の追加により、runStreamConnServer 関数がどのような経路で終了しても、done チャネルが確実にクローズされるようになりました。これにより、チャネルのリークを防ぎ、テストのクリーンアップが保証されます。また、done <- 1 の代わりに close(done) を使用することで、チャネルの完了通知がより明確になり、複数の受信側がチャネルのクローズを検知できるようになります。

t.Logf の追加

runStreamConnServer 関数内の Accept ループに t.Logf が追加されました。

変更前:

run:
    for {
        c, err := l.Accept()
        if err != nil {
            continue run // エラーを無視してループを継続
        }
        // ...
    }

変更後:

run:
    for {
        c, err := l.Accept()
        if err != nil {
            t.Logf("Accept failed: %v", err) // エラーをログに出力
            continue run
        }
        // ...
    }

l.Accept() がエラーを返した場合に、そのエラーメッセージが t.Logf を使ってテストログに出力されるようになりました。これにより、Accept が失敗した具体的な理由がテストログに記録され、デバッグ時の情報が豊富になります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のテストに関する一般的なプラクティス
  • Go言語のチャネルとゴルーチンに関する情報
  • Go言語のネットワークプログラミングに関する情報
  • Stack Overflowなどの開発者コミュニティでのGoテストに関する議論
  • Goのコードレビューシステム (Gerrit) の変更履歴