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

[インデックス 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.Bodyio.ReadCloser インターフェース型です。このインターフェースは、Read(p []byte) (n int, err error) メソッドと Close() error メソッドの2つを定義しています。

  • Read メソッドは、レスポンスボディのデータを読み取るために使用されます。
  • Close メソッドは、レスポンスボディに関連付けられたリソース(通常は基盤となるTCP接続)を解放するために呼び出す必要があります。

http.Gethttp.Client.Do などの関数が成功した場合、http.Response オブジェクトが返されますが、その Body はまだ開いた状態です。この Body を読み終えた後、または読み取る必要がない場合でも、必ず Close() メソッドを呼び出すことが重要です。これを怠ると、TCP接続が閉じられず、サーバー側で接続がタイムアウトするまでリソースが占有され続ける可能性があります。これは、特にクライアントが多数のHTTPリクエストを連続して行う場合に、接続プールの枯渇やパフォーマンスの低下につながります。

defer res.Body.Close() のベストプラクティス

defer res.Body.Close() は、net/http を使用する際の最も重要なベストプラクティスの一つです。これを適切に配置することで、リソースリークを防ぎ、アプリケーションの堅牢性を高めることができます。

理想的な配置は、http.Gethttp.Client.Do の呼び出しが成功し、*http.Response オブジェクトが有効になった直後です。また、リクエスト自体がエラーを返した場合(例: ネットワークエラー)は、resnil になる可能性があるため、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 がこの位置にあった

または、deferioutil.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 があった

この配置の問題点は以下の通りです。

  1. エラーパスでのリソースリークの可能性: ioutil.ReadAll(res.Body) がエラーを返した場合、例えばネットワーク接続が途中で切断されたり、無効なデータが受信されたりした場合、if err != nil { log.Fatal(err) } のブロックが実行され、関数がそこで終了します。もし defer res.Body.Close() がこのエラーチェックの後に配置されていた場合、defer された関数は実行されず、res.Body がクローズされないままリソースがリークする可能性があります。

  2. defer の意図との乖離: defer の主な目的は、関数の終了時にクリーンアップ処理を確実に実行することです。リソースが取得された直後に defer することで、そのリソースが関数のライフサイクル全体で適切に管理されることを保証できます。res.Bodyhttp.Gethttp.Client.Do が成功した時点で取得されるリソースであるため、その直後にクローズ処理を defer するのが最も安全で意図に沿った使い方です。

このコミットでは、defer res.Body.Close()res.Body が有効になった直後、つまり http.Gethttp.Client.Do の呼び出しと、その結果のエラーチェックの直後に移動しています。

// 修正後のコード (ExampleGet の一部)
	robots, err := ioutil.ReadAll(res.Body) // この行の前に defer が移動
	res.Body.Close() // defer ではなく直接呼び出しに変更された

example_test.goExampleGet 関数では、defer が削除され、res.Body.Close()ioutil.ReadAll の直後に直接呼び出されています。これは、ExampleGet がシンプルな例であり、log.Fatal で関数が終了するため、defer の必要性が低いと判断された可能性があります。しかし、より一般的なケースでは defer が推奨されます。

一方、transport_test.goTestTransportConnectionCloseOnResponse 関数では、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つのテストファイルです。

  1. src/pkg/net/http/example_test.go
  2. 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 ステートメントは、それが定義された関数が終了する直前に実行されることを保証します。deferres が有効になった直後に配置することで、ioutil.ReadAll(res.Body) の実行中にエラーが発生した場合でも、res.Body.Close() が確実に呼び出されるようになります。これにより、テスト実行中にリソースリークが発生する可能性が排除され、テストの信頼性が向上します。これは、Go言語における defer の最も推奨される使用パターンの一つであり、リソース管理のベストプラクティスを反映しています。

関連リンク

参考にした情報源リンク