[インデックス 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
の主な特徴は以下の通りです。
- 実行タイミング:
defer
に指定された関数は、それを囲む関数がreturn
する直前、またはパニックによって関数が終了する直前に実行されます。 - 評価タイミング:
defer
ステートメントに渡される関数の引数やレシーバは、defer
ステートメントが宣言された時点で評価されます。これは非常に重要で、例えばdefer fmt.Println(i)
のようなコードがあった場合、i
の値はdefer
が書かれた行で評価され、その値がPrintln
に渡されることになります。 - スタック: 複数の
defer
ステートメントがある場合、それらはLIFO(Last-In, First-Out)の順序で実行されます。つまり、最後に宣言されたdefer
が最初に実行されます。
nil
ポインタ間接参照 (nil pointer dereference)
Go言語では、ポインタが何も指していない状態を nil
と表現します。nil
ポインタに対してメソッドを呼び出したり、その指す値にアクセスしようとすると、「nil
ポインタ間接参照」というランタイムパニックが発生し、プログラムが異常終了します。
例えば、var f *os.File
と宣言された f
が nil
のまま f.Close()
を呼び出すとパニックになります。これは、Close
メソッドが *os.File
型のレシーバを期待しているにもかかわらず、nil
値に対してメソッドが呼び出されたためです。
エラーハンドリングの重要性
Go言語では、関数がエラーを返す場合、そのエラーを適切にチェックし、処理することが推奨されます。多くの場合、関数は成功時には有効な値と nil
エラーを返し、失敗時には無効な値(nil
など)と非nil
エラーを返します。
このコミットの文脈では、Open
のような関数が (file, err)
のように file
と err
の両方を返す場合、err
が nil
でない(エラーが発生した)ならば、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
メソッドをスケジュールします。このとき file
が nil
であっても、defer
自体はパニックを起こしません。パニックは、関数が実際に実行される(つまり、囲む関数がリターンする直前)ときに発生します。
したがって、Open
がエラーを返し、file
が nil
になった場合、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)
が成功し、err
が nil
であった場合のみ、defer file.Close()
が実行される行に到達します。この時点では file
は有効な(nil
でない)オブジェクトであることが保証されているため、Close
メソッドの呼び出しは安全に行われます。
この変更は、Go言語のベストプラクティスである「エラーを早期にチェックし、早期にリターンする」という原則にも合致しています。リソースのクリーンアップは、そのリソースが正常に取得された場合にのみスケジュールされるべきです。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルが変更されています。
src/pkg/net/http/transport_test.go
src/pkg/os/os_test.go
これらのファイル内の複数の箇所で、defer
ステートメントが ioutil.ReadAll
や Open
などの関数呼び出しと、それに続くエラーチェックの間にあったものが、エラーチェックの後に移動されています。
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()
がエラーを返し、resource
が nil
になった場合でも、(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()
が成功し、err
が nil
であった場合にのみ、コードの実行は 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
)