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

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

このコミットは、Go言語の標準ライブラリ内のテストコードにおいて、defer ステートメントの配置をエラーチェックの後ろに移動させる変更です。これにより、nil ポインタに対する間接参照(dereference)を回避し、潜在的なパニックを防ぐことが目的です。

コミット

commit deb53889c200deba1d4048a155e936c11b6a8492
Author: Rob Pike <r@golang.org>
Date:   Fri Aug 17 11:55:11 2012 -0700

    all: move defers to after error check to avoid nil indirection
    Only affects some tests and none seem likely to be problematic, but let's fix them.
    Fixes #3971.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/6463060

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

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

元コミット内容

all: move defers to after error check to avoid nil indirection Only affects some tests and none seem likely to be problematic, but let's fix them. Fixes #3971.

変更の背景

この変更の背景には、Go言語における defer ステートメントの評価タイミングと、nil ポインタに対する操作の安全性に関する考慮があります。

Go言語の defer ステートメントは、それが宣言された関数がリターンする直前、またはパニックが発生した場合に実行されるようにスケジュールされます。重要なのは、defer に渡される関数の引数やレシーバは、defer ステートメントが実行されたその時点で評価されるという点です。

コミットメッセージにある Fixes #3971 は、GoのIssueトラッカーにおける問題番号を示しています。このIssue(https://github.com/golang/go/issues/3971)では、defer ステートメントが、それが操作しようとするオブジェクト(例えばファイルハンドルやネットワーク接続)が nil である可能性があるエラーチェックの前に配置されている場合に、nil ポインタ間接参照によるパニックが発生する可能性があることが指摘されていました。

具体的には、Open 関数などがエラーを返した場合、その戻り値であるファイルオブジェクト(*os.File など)は nil になる可能性があります。この nil オブジェクトに対して defer file.Close() のように Close メソッドを呼び出そうとすると、nil ポインタ間接参照が発生し、プログラムがパニックに陥ります。

このコミットは、このような潜在的なパニックを回避するために、defer ステートメントを、関連するオブジェクトが正常に初期化されたことを確認するエラーチェックのに移動させることを目的としています。これにより、defer がスケジュールされる時点でオブジェクトが nil でないことが保証され、安全にリソースのクリーンアップが行えるようになります。

この変更は主にテストコードに影響を与えていますが、本番コードでも同様のパターンが存在する可能性があり、堅牢性を高めるための予防的な修正として行われました。

前提知識の解説

Go言語の defer ステートメント

defer ステートメントは、Go言語の重要な機能の一つで、関数の実行が終了する直前(returnする前、またはパニックが発生して関数が終了する前)に、指定された関数呼び出しをスケジュールするために使用されます。主にリソースの解放(ファイルクローズ、ロック解除、ネットワーク接続の切断など)や、クリーンアップ処理に利用されます。

defer の主な特徴は以下の通りです。

  1. 実行タイミング: defer に指定された関数は、それを囲む関数が return する直前、またはパニックによって関数が終了する直前に実行されます。
  2. 評価タイミング: defer ステートメントに渡される関数の引数やレシーバは、defer ステートメントが宣言された時点で評価されます。これは非常に重要で、例えば defer fmt.Println(i) のようなコードがあった場合、i の値は defer が書かれた行で評価され、その値が Println に渡されることになります。
  3. スタック: 複数の defer ステートメントがある場合、それらはLIFO(Last-In, First-Out)の順序で実行されます。つまり、最後に宣言された defer が最初に実行されます。

nil ポインタ間接参照 (nil pointer dereference)

Go言語では、ポインタが何も指していない状態を nil と表現します。nil ポインタに対してメソッドを呼び出したり、その指す値にアクセスしようとすると、「nil ポインタ間接参照」というランタイムパニックが発生し、プログラムが異常終了します。

例えば、var f *os.File と宣言された fnil のまま f.Close() を呼び出すとパニックになります。これは、Close メソッドが *os.File 型のレシーバを期待しているにもかかわらず、nil 値に対してメソッドが呼び出されたためです。

エラーハンドリングの重要性

Go言語では、関数がエラーを返す場合、そのエラーを適切にチェックし、処理することが推奨されます。多くの場合、関数は成功時には有効な値と nil エラーを返し、失敗時には無効な値(nil など)と非nil エラーを返します。

このコミットの文脈では、Open のような関数が (file, err) のように fileerr の両方を返す場合、errnil でない(エラーが発生した)ならば、file は通常 nil または無効な状態であると期待されます。したがって、file を使用する前に err をチェックすることが不可欠です。

技術的詳細

このコミットの技術的な核心は、defer ステートメントの評価タイミングと、Go言語の堅牢なエラーハンドリングの原則を組み合わせることにあります。

変更前は、以下のようなパターンが見られました。

file, err := Open(name)
defer file.Close() // ここでfileがnilの場合、パニックの可能性
if err != nil {
    t.Fatal("open failed:", err)
}
// fileを使った処理

このコードでは、Open(name) がエラーを返した場合、file 変数は nil になります。しかし、defer file.Close()defer ステートメントが宣言された時点で file の値を評価し、Close メソッドをスケジュールします。このとき filenil であっても、defer 自体はパニックを起こしません。パニックは、関数が実際に実行される(つまり、囲む関数がリターンする直前)ときに発生します。

したがって、Open がエラーを返し、filenil になった場合、defer file.Close()nil レシーバに対して Close メソッドを呼び出そうとし、ランタイムパニックを引き起こします。これは、エラーを適切に処理して t.Fatal で終了するはずのパスで発生するため、予期せぬプログラムのクラッシュにつながります。

変更後は、defer ステートメントがエラーチェックのに移動されています。

file, err := Open(name)
if err != nil {
    t.Fatal("open failed:", err)
}
defer file.Close() // ここではfileがnilでないことが保証される
// fileを使った処理

この修正により、Open(name) がエラーを返した場合、if err != nil ブロックが実行され、t.Fatal によって関数が終了します。このパスでは defer file.Close() は実行されません(defer がスケジュールされる前に関数が終了するため)。

一方、Open(name) が成功し、errnil であった場合のみ、defer file.Close() が実行される行に到達します。この時点では file は有効な(nil でない)オブジェクトであることが保証されているため、Close メソッドの呼び出しは安全に行われます。

この変更は、Go言語のベストプラクティスである「エラーを早期にチェックし、早期にリターンする」という原則にも合致しています。リソースのクリーンアップは、そのリソースが正常に取得された場合にのみスケジュールされるべきです。

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

このコミットでは、主に以下の2つのファイルが変更されています。

  1. src/pkg/net/http/transport_test.go
  2. src/pkg/os/os_test.go

これらのファイル内の複数の箇所で、defer ステートメントが ioutil.ReadAllOpen などの関数呼び出しと、それに続くエラーチェックの間にあったものが、エラーチェックの後に移動されています。

src/pkg/net/http/transport_test.go の変更例:

--- a/src/pkg/net/http/transport_test.go
+++ b/src/pkg/net/http/transport_test.go
@@ -161,10 +161,10 @@ func TestTransportConnectionCloseOnResponse(t *testing.T) {
 			t.Fatalf("error in connectionClose=%v, req #%d, Do: %v", connectionClose, n, err)
 		}
 		body, err := ioutil.ReadAll(res.Body)
-		defer res.Body.Close() // 変更前: ここでres.Bodyがnilの場合、パニックの可能性
 		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/os/os_test.go の変更例:

--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -69,10 +69,10 @@ var sysdir = func() (sd *sysDir) {
 
 func size(name string, t *testing.T) int64 {
 	file, err := Open(name)
-	defer file.Close() // 変更前: ここでfileがnilの場合、パニックの可能性
 	if err != nil {
 		t.Fatal("open failed:", err)
 	}
+	defer file.Close() // 変更後: エラーチェックの後ろに移動
 	var buf [100]byte
 	len := 0
 	for {

同様の変更が TestFstat, testReaddirnames, testReaddir, TestReaddirnamesOneAtATime などのテスト関数内でも行われています。

コアとなるコードの解説

変更の核心は、defer ステートメントの配置順序です。

変更前:

resource, err := acquireResource()
defer resource.Close() // (A)
if err != nil {
    // エラー処理
    return
}
// resource を使用する処理

このパターンでは、acquireResource() がエラーを返し、resourcenil になった場合でも、(A)defer resource.Close() はスケジュールされます。そして、関数が終了する際に nil.Close() が実行され、パニックが発生します。

変更後:

resource, err := acquireResource()
if err != nil {
    // エラー処理
    return
}
defer resource.Close() // (B)
// resource を使用する処理

このパターンでは、acquireResource() がエラーを返した場合、if err != nil ブロックが実行され、関数は return します。このとき、(B)defer ステートメントはまだ実行されていないため、スケジュールされることもありません。したがって、nil ポインタに対する Close 呼び出しは発生しません。

acquireResource() が成功し、errnil であった場合にのみ、コードの実行は defer resource.Close() の行に到達します。この時点では resource は有効な(nil でない)オブジェクトであることが保証されているため、defer は安全にスケジュールされ、関数終了時に正しく Close メソッドが呼び出されます。

この修正は、Go言語におけるリソース管理のベストプラクティスを反映しており、エラーパスでの予期せぬパニックを防ぎ、プログラムの堅牢性を向上させます。

関連リンク

  • Go言語の defer ステートメントに関する公式ドキュメントやチュートリアル
  • Go言語のエラーハンドリングに関する公式ドキュメントやベストプラクティスガイド
  • Go Issue #3971: https://github.com/golang/go/issues/3971

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GitHubのGoリポジトリのIssueトラッカー
  • Go言語に関する技術ブログや記事(defer とエラーハンドリング、nil ポインタについて解説しているもの)
  • Go言語のソースコード(src/pkg/net/http/transport_test.go および src/pkg/os/os_test.go