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

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

このコミットは、Go言語の標準ライブラリである net/http パッケージのベンチマークテストにおいて、HTTPレスポンスボディのクローズ処理を追加するものです。具体的には、ioutil.ReadAll でレスポンスボディを読み取った後に res.Body.Close() を呼び出すように修正されています。

コミット

commit a1aee55bd17b79edc66a865c02d170ed6296288d
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Feb 17 06:04:31 2014 +0400

    net/http: close body in benchmarks
    Is it required? Why don't we do it?
    
    R=bradfitz
    CC=golang-codereviews
    https://golang.org/cl/61150043

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

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

元コミット内容

net/http: close body in benchmarks Is it required? Why don't we do it?

このコミットメッセージは、ベンチマークテストにおいてHTTPレスポンスボディをクローズする必要性について疑問を呈し、その対応を行うことを示唆しています。

変更の背景

Goの net/http パッケージにおいて、HTTPリクエストを送信しレスポンスを受け取った際、レスポンスボディ(res.Body)は io.ReadCloser インターフェースを実装しています。これは、ボディの内容を読み取ることができる io.Reader と、リソースを解放するための Close() メソッドを持つ io.Closer の両方の特性を持つことを意味します。

HTTPレスポンスボディは、通常、ネットワーク接続やファイルディスクリプタなどのシステムリソースに関連付けられています。これらのリソースは有限であり、適切に解放されないとリソースリークを引き起こす可能性があります。特に、多数のリクエストを処理するサーバーや、繰り返しHTTPリクエストを行うベンチマークテストのようなシナリオでは、リソースリークが深刻な問題となり、最終的にはシステム全体のパフォーマンス低下やクラッシュにつながる可能性があります。

このコミットが行われた背景には、おそらく net/http のベンチマークテストが、レスポンスボディを読み取った後に Close() を呼び出していなかったため、テスト実行中にリソースが適切に解放されず、ベンチマーク結果の信頼性やテスト環境の安定性に影響を与えていた可能性が考えられます。コミットメッセージの「Is it required? Why don't we do it?」という問いかけは、この問題意識を明確に示しています。

前提知識の解説

Go言語の net/http パッケージ

net/http パッケージは、Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。WebアプリケーションやAPIの構築に広く利用されます。

http.ResponseBody

HTTPリクエストを送信すると、http.Response 構造体が返されます。この構造体には、ステータスコード、ヘッダー、そしてレスポンスボディが含まれます。レスポンスボディは Body フィールドとして io.ReadCloser 型で提供されます。

io.ReadCloser インターフェース

io.ReadCloser は、Goの標準ライブラリ io パッケージで定義されているインターフェースです。これは以下の2つのインターフェースを組み合わせたものです。

  • io.Reader: Read(p []byte) (n int, err error) メソッドを持ち、データを読み取る機能を提供します。
  • io.Closer: Close() error メソッドを持ち、リソースを解放する機能を提供します。

http.Response.Bodyio.ReadCloser であるということは、レスポンスボディからデータを読み取ることができるだけでなく、読み取りが完了した後にそのボディに関連付けられたリソースを明示的にクローズする必要があることを意味します。

ioutil.ReadAll

ioutil.ReadAll (Go 1.16以降は io.ReadAll に移動) は、io.Reader からすべてのデータを読み取り、[]byte スライスとして返すユーティリティ関数です。この関数は、指定された Reader のEOF (End Of File) に達するまで読み取りを続けます。

リソースリーク

プログラムが確保したメモリ、ファイルディスクリプタ、ネットワークソケットなどのシステムリソースを、使用後に適切に解放しない場合に発生する問題です。リソースリークが発生すると、利用可能なリソースが徐々に枯渇し、最終的にはプログラムの動作が不安定になったり、クラッシュしたりする原因となります。HTTPレスポンスボディの場合、基盤となるTCP接続や関連するバッファが解放されないことがリークにつながります。

defer ステートメント

Go言語の defer ステートメントは、関数がリターンする直前に実行される関数呼び出しをスケジュールするために使用されます。これは、リソースの解放(ファイルクローズ、ロック解除など)を確実に行うための非常に便利なメカニズムです。defer を使用することで、エラーハンドリングパスを含め、関数のどの終了パスでもリソースがクローズされることを保証できます。

技術的詳細

このコミットの技術的な核心は、HTTPレスポンスボディの Close() メソッドの呼び出しを、ioutil.ReadAll でボディの内容を完全に読み取った後に行うように修正した点にあります。

http.Client がHTTPリクエストを送信し、http.Response を受け取ると、その Response.Body は基盤となるTCP接続に関連付けられています。この接続は、レスポンスボディの読み取りが完了するか、または Close() メソッドが明示的に呼び出されるまで開いたままになる可能性があります。

ioutil.ReadAll(res.Body) は、レスポンスボディの内容をすべて読み取りますが、それ自体が res.Body.Close() を呼び出すわけではありません。したがって、ioutil.ReadAll の呼び出しだけでは、基盤となるネットワーク接続が適切に閉じられず、リソースリークが発生する可能性があります。特に、ベンチマークテストのように短期間に大量のHTTPリクエストを生成し、そのレスポンスを処理するシナリオでは、開かれたままの接続が蓄積され、ポート枯渇やメモリ消費の増大といった問題を引き起こす可能性が高まります。

このコミットでは、以下の3つのベンチマーク関数 (BenchmarkClientServer, benchmarkClientServerParallel, BenchmarkServer) において、ioutil.ReadAll(res.Body) の直後に res.Body.Close() を追加しています。これにより、レスポンスボディの読み取りが完了した時点で、関連するリソースが確実に解放されるようになります。

また、BenchmarkClientServer 関数では、defer res.Body.Close() も追加されています。これは、ioutil.ReadAll の前に defer を置くことで、ioutil.ReadAll の実行中にエラーが発生した場合でも res.Body.Close() が確実に呼び出されるようにするための防御的なプログラミングプラクティスです。ただし、ioutil.ReadAll がボディを完全に読み取った後、res.Body.Close() を明示的に呼び出すことで、defer が実行される前にリソースを早期に解放できるという利点もあります。このコミットでは両方が追加されており、堅牢性と即時性の両方を考慮した変更と言えます。

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

変更は src/pkg/net/http/serve_test.go ファイルに対して行われています。

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -2258,7 +2258,9 @@ func BenchmarkClientServer(b *testing.B) {
 		if err != nil {
 			b.Fatal("Get:", err)
 		}
+		defer res.Body.Close() // 追加
 		all, err := ioutil.ReadAll(res.Body)
+		res.Body.Close() // 追加
 		if err != nil {
 			b.Fatal("ReadAll:", err)
 		}
@@ -2301,6 +2303,7 @@ func benchmarkClientServerParallel(b *testing.B, conc int) {
 					continue
 				}
 				all, err := ioutil.ReadAll(res.Body)
+				res.Body.Close() // 追加
 				if err != nil {
 					b.Logf("ReadAll: %v", err)
 					continue
@@ -2338,6 +2341,7 @@ func BenchmarkServer(b *testing.B) {
 				log.Panicf("Get: %v", err)
 			}
 			all, err := ioutil.ReadAll(res.Body)
+			res.Body.Close() // 追加
 			if err != nil {
 				log.Panicf("ReadAll: %v", err)
 			}

コアとなるコードの解説

BenchmarkClientServer 関数

元のコードでは、ioutil.ReadAll(res.Body) でレスポンスボディを読み取った後、res.Body.Close() が呼び出されていませんでした。 変更後、以下の2行が追加されました。

  1. defer res.Body.Close(): Get メソッドの呼び出し直後に defer を使って res.Body.Close() をスケジュールしています。これにより、BenchmarkClientServer 関数が終了する際に、エラーが発生した場合でも確実にボディがクローズされるようになります。
  2. res.Body.Close(): ioutil.ReadAll(res.Body) の直後にも res.Body.Close() が追加されています。これは、defer によるクローズよりも早くリソースを解放することを意図していると考えられます。ioutil.ReadAll がボディを完全に読み取った後、すぐに接続を解放することで、リソースの占有時間を最小限に抑えることができます。

benchmarkClientServerParallel 関数

この並列ベンチマーク関数でも、ioutil.ReadAll(res.Body) の直後に res.Body.Close() が追加されました。これにより、各並列ゴルーチンがレスポンスボディの読み取りを終えるたびに、関連するリソースが解放されるようになります。並列処理ではリソースの枯渇がより顕著になるため、この修正は特に重要です。

BenchmarkServer 関数

このサーバーベンチマーク関数でも同様に、ioutil.ReadAll(res.Body) の直後に res.Body.Close() が追加されました。これにより、サーバー側でレスポンスボディを処理する際のクリーンアップが保証されます。

これらの変更により、net/http のベンチマークテストがより正確で安定したものになり、リソースリークによるテスト結果への影響が排除されます。これは、Goのネットワークプログラミングにおける「使用後は必ず Close() する」という重要なプラクティスをベンチマークコードにも適用した良い例です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語の net/http パッケージに関する一般的な情報源 (例: Go by Example, Go言語のブログ記事など)
  • Go言語における defer ステートメントの利用に関する情報源
  • Go言語におけるリソース管理とリークに関する情報源
  • コミットメッセージとコード差分

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

このコミットは、Go言語の標準ライブラリである net/http パッケージのベンチマークテストにおいて、HTTPレスポンスボディのクローズ処理を追加するものです。具体的には、ioutil.ReadAll でレスポンスボディを読み取った後に res.Body.Close() を呼び出すように修正されています。

コミット

commit a1aee55bd17b79edc66a865c02d170ed6296288d
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Feb 17 06:04:31 2014 +0400

    net/http: close body in benchmarks
    Is it required? Why don't we do it?
    
    R=bradfitz
    CC=golang-codereviews
    https://golang.org/cl/61150043

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

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

元コミット内容

net/http: close body in benchmarks Is it required? Why don't we do it?

このコミットメッセージは、ベンチマークテストにおいてHTTPレスポンスボディをクローズする必要性について疑問を呈し、その対応を行うことを示唆しています。

変更の背景

Goの net/http パッケージにおいて、HTTPリクエストを送信しレスポンスを受け取った際、レスポンスボディ(res.Body)は io.ReadCloser インターフェースを実装しています。これは、ボディの内容を読み取ることができる io.Reader と、リソースを解放するための Close() メソッドを持つ io.Closer の両方の特性を持つことを意味します。

HTTPレスポンスボディは、通常、ネットワーク接続やファイルディスクリプタなどのシステムリソースに関連付けられています。これらのリソースは有限であり、適切に解放されないとリソースリークを引き起こす可能性があります。特に、多数のリクエストを処理するサーバーや、繰り返しHTTPリクエストを行うベンチマークテストのようなシナリオでは、リソースリークが深刻な問題となり、最終的にはシステム全体のパフォーマンス低下やクラッシュにつながる可能性があります。

このコミットが行われた背景には、おそらく net/http のベンチマークテストが、レスポンスボディを読み取った後に Close() を呼び出していなかったため、テスト実行中にリソースが適切に解放されず、ベンチマーク結果の信頼性やテスト環境の安定性に影響を与えていた可能性が考えられます。コミットメッセージの「Is it required? Why don't we do it?」という問いかけは、この問題意識を明確に示しています。res.Body.Close() を呼び出さないと、基盤となるネットワーク接続が閉じられず、ゴルーチンリーク、ファイルディスクリプタの枯渇、メモリリークといった問題を引き起こす可能性があります。また、HTTPクライアントの接続再利用(Keep-Alive)も、レスポンスボディが完全に読み取られ、かつクローズされた場合にのみ行われるため、Close() の呼び出しはパフォーマンスにも影響します。

前提知識の解説

Go言語の net/http パッケージ

net/http パッケージは、Go言語でHTTPクライアントおよびサーバーを構築するための標準ライブラリです。WebアプリケーションやAPIの構築に広く利用されます。

http.ResponseBody

HTTPリクエストを送信すると、http.Response 構造体が返されます。この構造体には、ステータスコード、ヘッダー、そしてレスポンスボディが含まれます。レスポンスボディは Body フィールドとして io.ReadCloser 型で提供されます。

io.ReadCloser インターフェース

io.ReadCloser は、Goの標準ライブラリ io パッケージで定義されているインターフェースです。これは以下の2つのインターフェースを組み合わせたものです。

  • io.Reader: Read(p []byte) (n int, err error) メソッドを持ち、データを読み取る機能を提供します。
  • io.Closer: Close() error メソッドを持ち、リソースを解放する機能を提供します。

http.Response.Bodyio.ReadCloser であるということは、レスポンスボディからデータを読み取ることができるだけでなく、読み取りが完了した後にそのボディに関連付けられたリソースを明示的にクローズする必要があることを意味します。

ioutil.ReadAll

ioutil.ReadAll (Go 1.16以降は io.ReadAll に移動) は、io.Reader からすべてのデータを読み取り、[]byte スライスとして返すユーティリティ関数です。この関数は、指定された Reader のEOF (End Of File) に達するまで読み取りを続けます。

リソースリーク

プログラムが確保したメモリ、ファイルディスクリプタ、ネットワークソケットなどのシステムリソースを、使用後に適切に解放しない場合に発生する問題です。リソースリークが発生すると、利用可能なリソースが徐々に枯渇し、最終的にはプログラムの動作が不安定になったり、クラッシュしたりする原因となります。HTTPレスポンスボディの場合、基盤となるTCP接続や関連するバッファが解放されないことがリークにつながります。

defer ステートメント

Go言語の defer ステートメントは、関数がリターンする直前に実行される関数呼び出しをスケジュールするために使用されます。これは、リソースの解放(ファイルクローズ、ロック解除など)を確実に行うための非常に便利なメカニズムです。defer を使用することで、エラーハンドリングパスを含め、関数のどの終了パスでもリソースがクローズされることを保証できます。

技術的詳細

このコミットの技術的な核心は、HTTPレスポンスボディの Close() メソッドの呼び出しを、ioutil.ReadAll でボディの内容を完全に読み取った後に行うように修正した点にあります。

http.Client がHTTPリクエストを送信し、http.Response を受け取ると、その Response.Body は基盤となるTCP接続に関連付けられています。この接続は、レスポンスボディの読み取りが完了するか、または Close() メソッドが明示的に呼び出されるまで開いたままになる可能性があります。

ioutil.ReadAll(res.Body) は、レスポンスボディの内容をすべて読み取りますが、それ自体が res.Body.Close() を呼び出すわけではありません。したがって、ioutil.ReadAll の呼び出しだけでは、基盤となるネットワーク接続が適切に閉じられず、リソースリークが発生する可能性があります。特に、ベンチマークテストのように短期間に大量のHTTPリクエストを生成し、そのレスポンスを処理するシナリオでは、開かれたままの接続が蓄積され、ポート枯渇やメモリ消費の増大といった問題を引き起こす可能性が高まります。

このコミットでは、以下の3つのベンチマーク関数 (BenchmarkClientServer, benchmarkClientServerParallel, BenchmarkServer) において、ioutil.ReadAll(res.Body) の直後に res.Body.Close() を追加しています。これにより、レスポンスボディの読み取りが完了した時点で、関連するリソースが確実に解放されるようになります。

また、BenchmarkClientServer 関数では、defer res.Body.Close() も追加されています。これは、ioutil.ReadAll の前に defer を置くことで、ioutil.ReadAll の実行中にエラーが発生した場合でも res.Body.Close() が確実に呼び出されるようにするための防御的なプログラミングプラクティスです。ただし、ioutil.ReadAll がボディを完全に読み取った後、res.Body.Close() を明示的に呼び出すことで、defer が実行される前にリソースを早期に解放できるという利点もあります。このコミットでは両方が追加されており、堅牢性と即時性の両方を考慮した変更と言えます。

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

変更は src/pkg/net/http/serve_test.go ファイルに対して行われています。

--- a/src/pkg/net/http/serve_test.go
+++ b/src/pkg/net/http/serve_test.go
@@ -2258,7 +2258,9 @@ func BenchmarkClientServer(b *testing.B) {
 		if err != nil {
 			b.Fatal("Get:", err)
 		}
+		defer res.Body.Close() // 追加
 		all, err := ioutil.ReadAll(res.Body)
+		res.Body.Close() // 追加
 		if err != nil {
 			b.Fatal("ReadAll:", err)
 		}
@@ -2301,6 +2303,7 @@ func benchmarkClientServerParallel(b *testing.B, conc int) {
 					continue
 				}
 				all, err := ioutil.ReadAll(res.Body)
+				res.Body.Close() // 追加
 				if err != nil {
 					b.Logf("ReadAll: %v", err)
 					continue
@@ -2338,6 +2341,7 @@ func BenchmarkServer(b *testing.B) {
 				log.Panicf("Get: %v", err)
 			}
 			all, err := ioutil.ReadAll(res.Body)
+			res.Body.Close() // 追加
 			if err != nil {
 				log.Panicf("ReadAll: %v", err)
 			}

コアとなるコードの解説

BenchmarkClientServer 関数

元のコードでは、ioutil.ReadAll(res.Body) でレスポンスボディを読み取った後、res.Body.Close() が呼び出されていませんでした。 変更後、以下の2行が追加されました。

  1. defer res.Body.Close(): Get メソッドの呼び出し直後に defer を使って res.Body.Close() をスケジュールしています。これにより、BenchmarkClientServer 関数が終了する際に、エラーが発生した場合でも確実にボディがクローズされるようになります。
  2. res.Body.Close(): ioutil.ReadAll(res.Body) の直後にも res.Body.Close() が追加されています。これは、defer によるクローズよりも早くリソースを解放することを意図していると考えられます。ioutil.ReadAll がボディを完全に読み取った後、すぐに接続を解放することで、リソースの占有時間を最小限に抑えることができます。

benchmarkClientServerParallel 関数

この並列ベンチマーク関数でも、ioutil.ReadAll(res.Body) の直後に res.Body.Close() が追加されました。これにより、各並列ゴルーチンがレスポンスボディの読み取りを終えるたびに、関連するリソースが解放されるようになります。並列処理ではリソースの枯渇がより顕著になるため、この修正は特に重要です。

BenchmarkServer 関数

このサーバーベンチマーク関数でも同様に、ioutil.ReadAll(res.Body) の直後に res.Body.Close() が追加されました。これにより、サーバー側でレスポンスボディを処理する際のクリーンアップが保証されます。

これらの変更により、net/http のベンチマークテストがより正確で安定したものになり、リソースリークによるテスト結果への影響が排除されます。これは、Goのネットワークプログラミングにおける「使用後は必ず Close() する」という重要なプラクティスをベンチマークコードにも適用した良い例です。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語の net/http パッケージに関する一般的な情報源 (例: Go by Example, Go言語のブログ記事など)
  • Go言語における defer ステートメントの利用に関する情報源
  • Go言語におけるリソース管理とリークに関する情報源
  • コミットメッセージとコード差分
  • Web検索結果: "Go net/http res.Body.Close() importance" (特に、リソースリーク、ゴルーチンリーク、ファイルディスクリプタ枯渇、接続再利用に関する情報)