[インデックス 13662] ファイルの概要
このコミットは、Go言語の標準ライブラリ net/http
パッケージ内のテストコードおよび例における defer res.Body.Close()
の配置ミスを修正し、ドキュメントの例を実際の動作に合わせることを目的としています。具体的には、res.Body.Close()
の defer
ステートメントが、ioutil.ReadAll
の呼び出し後に配置されていたのを、res.Body
が有効になった直後に移動することで、リソースリークの可能性を防ぎ、より堅牢なコードパターンを確立しています。
コミット
commit 46c9346d749d159190ed8058625e1bdb3a614989
Author: Dave Cheney <dave@cheney.net>
Date: Tue Aug 21 11:46:07 2012 +1000
net/http: fix misplaced defer and example
Moves the defer (again).
Also, correct the example documentation to match.
R=r, robert.hencke, iant, dsymonds, bradfitz
CC=golang-dev
https://golang.org/cl/6458158
---
src/pkg/net/http/example_test.go | 2 +-\n src/pkg/net/http/transport_test.go | 2 +-\n 2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/pkg/net/http/example_test.go b/src/pkg/net/http/example_test.go
index ec814407dd..22073eaf7a 100644
--- a/src/pkg/net/http/example_test.go
+++ b/src/pkg/net/http/example_test.go
@@ -43,10 +43,10 @@ func ExampleGet() {\n log.Fatal(err)\n }\n robots, err := ioutil.ReadAll(res.Body)\n+\tres.Body.Close()\n if err != nil {\n \t\tlog.Fatal(err)\n \t}\n-\tres.Body.Close()\n fmt.Printf("%s", robots)\n }\
\ndiff --git a/src/pkg/net/http/transport_test.go b/src/pkg/net/http/transport_test.go
index 14465727c2..e4072e88fe 100644
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -160,11 +160,11 @@ func TestTransportConnectionCloseOnResponse(t *testing.T) {\n \t\t\tif err != nil {\n \t\t\t\tt.Fatalf("error in connectionClose=%v, req #%d, Do: %v", connectionClose, n, err)\n \t\t\t}\n+\t\t\tdefer res.Body.Close()\n \t\t\tbody, err := ioutil.ReadAll(res.Body)\n \t\t\tif err != nil {\n \t\t\t\tt.Fatalf("error in connectionClose=%v, req #%d, ReadAll: %v", connectionClose, n, err)\n \t\t\t}\n-\t\t\tdefer res.Body.Close()\n \t\t\treturn string(body)\n \t\t}\
\n```
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/46c9346d749d159190ed8058625e1bdb3a614989](https://github.com/golang/go/commit/46c9346d749d159190ed8058625e1bdb3a614989)
## 元コミット内容
このコミットは、`net/http` パッケージにおける `defer` ステートメントの配置を修正し、関連するドキュメントの例を更新するものです。具体的には、`res.Body.Close()` の `defer` 呼び出しが誤った位置にあったため、それを適切な位置に移動し、それに合わせて例のドキュメントも修正しています。
## 変更の背景
Go言語の `net/http` パッケージでHTTPリクエストを行う際、レスポンスボディ (`res.Body`) は `io.ReadCloser` インターフェースを実装しており、これはストリームとして扱われます。このストリームは、基盤となるネットワーク接続やシステムリソースを解放するために、明示的にクローズされる必要があります。もし `res.Body` が適切にクローズされない場合、特に多数のHTTPリクエストを行うアプリケーションでは、リソースリーク(例: ファイルディスクリプタの枯渇、ネットワーク接続の占有)が発生する可能性があります。
`defer` キーワードは、Goにおいて関数の終了時に特定の処理を実行することを保証するための強力なメカニズムです。しかし、`defer` の配置が不適切だと、意図しない動作やリソースリークを引き起こす可能性があります。このコミットの背景には、`res.Body.Close()` の `defer` が、`res.Body` からデータを読み取った後に配置されていたという問題がありました。これは、`ioutil.ReadAll` がエラーを返した場合など、ボディの読み取りが完了する前にエラーが発生した場合に、`res.Body.Close()` が呼び出されない可能性を生じさせます。このような状況では、リソースが解放されずに残り、アプリケーションの安定性やパフォーマンスに悪影響を与える可能性があります。
このコミットは、このような潜在的なリソースリークを防ぎ、`net/http` を利用する際のベストプラクティスをコード例とテストコードで示すために行われました。
## 前提知識の解説
### Go言語の `defer` キーワード
`defer` ステートメントは、Go言語のユニークな機能の一つで、そのステートメントが属する関数がリターンする直前に、指定された関数呼び出しを実行することを保証します。これは、リソースのクリーンアップ(ファイルハンドルのクローズ、ロックの解放、ネットワーク接続のクローズなど)を確実に行うために非常に有用です。`defer` は、関数の正常終了時だけでなく、パニックが発生した場合でも実行されるため、堅牢なエラーハンドリングとリソース管理に貢献します。
```go
func readFile(filename string) ([]byte, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
// defer f.Close() は、readFile 関数が終了する直前に実行される
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
return data, nil
}
上記の例では、f.Close()
が defer
されているため、readFile
関数が正常に終了しても、エラーで終了しても、ファイルは確実にクローズされます。
net/http
パッケージと res.Body
Goの net/http
パッケージは、HTTPクライアントとサーバーの実装を提供します。HTTPリクエストを送信し、レスポンスを受け取る際、レスポンスオブジェクト (*http.Response
) には Body
フィールドが含まれています。
http.Response.Body
は io.ReadCloser
インターフェース型です。このインターフェースは、Read(p []byte) (n int, err error)
メソッドと Close() error
メソッドの2つを定義しています。
Read
メソッドは、レスポンスボディのデータを読み取るために使用されます。Close
メソッドは、レスポンスボディに関連付けられたリソース(通常は基盤となるTCP接続)を解放するために呼び出す必要があります。
http.Get
や http.Client.Do
などの関数が成功した場合、http.Response
オブジェクトが返されますが、その Body
はまだ開いた状態です。この Body
を読み終えた後、または読み取る必要がない場合でも、必ず Close()
メソッドを呼び出すことが重要です。これを怠ると、TCP接続が閉じられず、サーバー側で接続がタイムアウトするまでリソースが占有され続ける可能性があります。これは、特にクライアントが多数のHTTPリクエストを連続して行う場合に、接続プールの枯渇やパフォーマンスの低下につながります。
defer res.Body.Close()
のベストプラクティス
defer res.Body.Close()
は、net/http
を使用する際の最も重要なベストプラクティスの一つです。これを適切に配置することで、リソースリークを防ぎ、アプリケーションの堅牢性を高めることができます。
理想的な配置は、http.Get
や http.Client.Do
の呼び出しが成功し、*http.Response
オブジェクトが有効になった直後です。また、リクエスト自体がエラーを返した場合(例: ネットワークエラー)は、res
が nil
になる可能性があるため、defer
の前にエラーチェックを行う必要があります。
resp, err := http.Get("http://example.com")
if err != nil {
// リクエスト自体が失敗した場合のハンドリング
return err
}
// resp が有効になった直後に defer を配置する
defer resp.Body.Close()
// ここで resp.Body からデータを読み取る
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// ボディの読み取り中にエラーが発生した場合のハンドリング
return err
}
// body を処理する
このように配置することで、resp.Body
からの読み取りが成功しても失敗しても、あるいは関数が途中でリターンしても、resp.Body.Close()
が確実に呼び出され、リソースが解放されます。
技術的詳細
このコミットの技術的詳細は、defer
ステートメントの実行タイミングと、net/http
レスポンスボディのライフサイクル管理に集約されます。
元のコードでは、defer res.Body.Close()
が ioutil.ReadAll(res.Body)
の呼び出し後に配置されていました。
// 元のコード (ExampleGet の一部)
robots, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatal(err)
}
res.Body.Close() // defer ではなく直接呼び出し、または defer がこの位置にあった
または、defer
が ioutil.ReadAll
の後にあった場合:
// 元のコード (TestTransportConnectionCloseOnResponse の一部)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("error in connectionClose=%v, req #%d, ReadAll: %v", connectionClose, n, err)
}
defer res.Body.Close() // ここに defer があった
この配置の問題点は以下の通りです。
-
エラーパスでのリソースリークの可能性:
ioutil.ReadAll(res.Body)
がエラーを返した場合、例えばネットワーク接続が途中で切断されたり、無効なデータが受信されたりした場合、if err != nil { log.Fatal(err) }
のブロックが実行され、関数がそこで終了します。もしdefer res.Body.Close()
がこのエラーチェックの後に配置されていた場合、defer
された関数は実行されず、res.Body
がクローズされないままリソースがリークする可能性があります。 -
defer
の意図との乖離:defer
の主な目的は、関数の終了時にクリーンアップ処理を確実に実行することです。リソースが取得された直後にdefer
することで、そのリソースが関数のライフサイクル全体で適切に管理されることを保証できます。res.Body
はhttp.Get
やhttp.Client.Do
が成功した時点で取得されるリソースであるため、その直後にクローズ処理をdefer
するのが最も安全で意図に沿った使い方です。
このコミットでは、defer res.Body.Close()
を res.Body
が有効になった直後、つまり http.Get
や http.Client.Do
の呼び出しと、その結果のエラーチェックの直後に移動しています。
// 修正後のコード (ExampleGet の一部)
robots, err := ioutil.ReadAll(res.Body) // この行の前に defer が移動
res.Body.Close() // defer ではなく直接呼び出しに変更された
example_test.go
の ExampleGet
関数では、defer
が削除され、res.Body.Close()
が ioutil.ReadAll
の直後に直接呼び出されています。これは、ExampleGet
がシンプルな例であり、log.Fatal
で関数が終了するため、defer
の必要性が低いと判断された可能性があります。しかし、より一般的なケースでは defer
が推奨されます。
一方、transport_test.go
の TestTransportConnectionCloseOnResponse
関数では、defer res.Body.Close()
が ioutil.ReadAll(res.Body)
の呼び出しの前に移動されています。
// 修正後のコード (TestTransportConnectionCloseOnResponse の一部)
if err != nil {
t.Fatalf("error in connectionClose=%v, req #%d, Do: %v", connectionClose, n, err)
}
defer res.Body.Close() // ここに defer が移動
body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("error in connectionClose=%v, req #%d, ReadAll: %v", connectionClose, n, err)
}
この変更により、res.Body
が有効になった直後に defer
が設定されるため、ioutil.ReadAll
がエラーを返した場合でも、res.Body.Close()
が確実に実行されるようになります。これにより、テストケースにおけるリソースリークの可能性が排除され、より堅牢なテストが実現されます。
この修正は、Go言語におけるリソース管理のベストプラクティスを反映しており、特にネットワークI/Oを伴うアプリケーション開発において非常に重要です。
コアとなるコードの変更箇所
このコミットで変更されたコアとなるコードは、以下の2つのテストファイルです。
src/pkg/net/http/example_test.go
src/pkg/net/http/transport_test.go
それぞれのファイルの変更点は以下の通りです。
src/pkg/net/http/example_test.go
--- a/src/pkg/net/http/example_test.go
+++ b/src/pkg/net/http/example_test.go
@@ -43,10 +43,10 @@ func ExampleGet() {
log.Fatal(err)
}
robots, err := ioutil.ReadAll(res.Body)
+ res.Body.Close()
if err != nil {
log.Fatal(err)
}
- res.Body.Close()
fmt.Printf("%s", robots)
}
src/pkg/net/http/transport_test.go
--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -160,11 +160,11 @@ func TestTransportConnectionCloseOnResponse(t *testing.T) {
if err != nil {
t.Fatalf("error in connectionClose=%v, req #%d, Do: %v", connectionClose, n, err)
}
+ defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Fatalf("error in connectionClose=%v, req #%d, ReadAll: %v", connectionClose, n, err)
}
- defer res.Body.Close()
return string(body)
}
コアとなるコードの解説
src/pkg/net/http/example_test.go
の変更
ExampleGet
関数は、http.Get
を使用してHTTPリクエストを送信し、レスポンスボディを読み取る例です。
元のコードでは、res.Body.Close()
が ioutil.ReadAll(res.Body)
の呼び出しと、その後のエラーチェックの後に直接呼び出されていました。
修正では、res.Body.Close()
の位置が ioutil.ReadAll(res.Body)
の直後に移動しています。これは、ioutil.ReadAll
がボディの内容をすべて読み取った直後にボディをクローズするという意図を明確にしています。この例では log.Fatal(err)
が使用されているため、エラーが発生した場合はプログラムが終了するため、defer
を使用するよりも直接クローズする方がシンプルで分かりやすいと判断された可能性があります。しかし、一般的なアプリケーションコードでは、エラーハンドリングを適切に行い、defer
を使用してリソースの解放を保証することが推奨されます。
src/pkg/net/http/transport_test.go
の変更
TestTransportConnectionCloseOnResponse
関数は、HTTPトランスポートの接続クローズ動作をテストするためのものです。このテスト関数内では、HTTPリクエストがループ内で複数回実行されます。
元のコードでは、defer res.Body.Close()
が ioutil.ReadAll(res.Body)
の呼び出しと、その後のエラーチェックの後に配置されていました。
修正では、defer res.Body.Close()
が http.Client.Do
(またはそれに相当するリクエスト実行) の呼び出しが成功し、res
オブジェクトが有効になった直後に移動されています。
この変更は非常に重要です。defer
ステートメントは、それが定義された関数が終了する直前に実行されることを保証します。defer
を res
が有効になった直後に配置することで、ioutil.ReadAll(res.Body)
の実行中にエラーが発生した場合でも、res.Body.Close()
が確実に呼び出されるようになります。これにより、テスト実行中にリソースリークが発生する可能性が排除され、テストの信頼性が向上します。これは、Go言語における defer
の最も推奨される使用パターンの一つであり、リソース管理のベストプラクティスを反映しています。
関連リンク
- Go CL 6458158: https://golang.org/cl/6458158
参考にした情報源リンク
- Web search results for "Go net/http defer res.Body.Close() best practices" (Google Search)
- https://www.google.com/search?q=Go+net%2Fhttp+defer+res.Body.Close%28%29+best+practices
- 特に、
defer res.Body.Close()
の重要性、defer
の配置、およびボディの読み取りに関するベストプラクティスについて参考にしました。