[インデックス 1575] ファイルの概要
このコミットで追加されたファイルは test/defer.go
です。このファイルはGo言語の defer
ステートメントの動作を検証するためのテストコードを含んでいます。具体的には、defer
された関数の実行順序(LIFO: Last-In, First-Out)と、defer
ステートメントが評価された時点で引数が評価されるという重要な特性をテストしています。
コミット
commit c8476472d900801b4b25fcdf6eb6bb79d80a45f5
Author: Rob Pike <r@golang.org>
Date: Tue Jan 27 15:08:08 2009 -0800
test for defer
R=rsc
DELTA=48 (48 added, 0 deleted, 0 changed)
OCL=23624
CL=23626
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c8476472d900801b4b25fcdf6eb6bb79d80a45f5
元コミット内容
test for defer
R=rsc
DELTA=48 (48 added, 0 deleted, 0 changed)
OCL=23624
CL=23626
変更の背景
このコミットは2009年1月27日に行われており、Go言語がまだ初期開発段階にあった時期に当たります。defer
ステートメントはGo言語の重要な特徴の一つであり、リソースの解放(ファイルクローズ、ロック解除など)やエラーハンドリングにおいて非常に便利です。しかし、その動作、特に複数の defer
がスタックされる場合の実行順序や、defer
された関数の引数がいつ評価されるかといった点は、言語の設計において明確に定義され、正しく実装されていることを保証する必要があります。
この時期はGo言語のコンパイラやランタイムが活発に開発されており、言語仕様の各要素が期待通りに機能するかどうかを検証するためのテストが継続的に追加されていました。defer
のような制御フローに関わる重要な機能については、その正確な動作を保証するための堅牢なテストケースが不可欠です。このコミットは、defer
の基本的な動作が正しく実装されていることを確認するための、初期の重要なテストの一つとして追加されました。
前提知識の解説
Go言語の defer
ステートメント
Go言語の defer
ステートメントは、関数がリターンする直前に実行される関数呼び出しをスケジュールするために使用されます。これは、リソースのクリーンアップ(ファイルのクローズ、データベース接続の解放、ミューテックスのアンロックなど)を確実に行うための非常に便利な機能です。
defer
の主な特徴は以下の通りです。
- 実行タイミング:
defer
された関数は、それを囲む関数がreturn
する直前、またはパニックによって終了する直前に実行されます。 - LIFO (Last-In, First-Out) 順序: 同じ関数内で複数の
defer
ステートメントが呼び出された場合、それらはスタックのようにLIFOの順序で実行されます。つまり、最後にdefer
された関数が最初に実行され、最初にdefer
された関数が最後に実行されます。 - 引数の評価タイミング:
defer
された関数の引数は、defer
ステートメントが実行されたその時点で評価されます。defer
された関数が実際に実行される時ではありません。これは非常に重要な特性であり、このコミットのテストでも検証されています。
例:
func example() {
i := 0
defer fmt.Println(i) // ここで i は 0 として評価され、記憶される
i++
fmt.Println("Inside example")
// example関数がリターンする直前に defer fmt.Println(0) が実行される
}
// 出力:
// Inside example
// 0
この特性により、defer
はリソースのクリーンアップに特に適しています。例えば、ファイルを開いた直後に defer file.Close()
と記述することで、関数のどこでリターンしても確実にファイルが閉じられることを保証できます。
技術的詳細
このコミットで追加された test/defer.go
は、defer
ステートメントの2つの主要な動作をテストしています。
-
LIFO (Last-In, First-Out) 実行順序の検証:
test1helper
関数とtest2helper
関数内でループを使って複数のdefer
ステートメントを呼び出しています。for i := 0; i < 10; i++ { defer addInt(i) }
このループでは、i
が0
から9
まで増加しながらdefer addInt(i)
が呼び出されます。defer
のLIFO特性により、addInt(9)
が最初に実行され、次にaddInt(8)
、...、最後にaddInt(0)
が実行されることを期待しています。 したがって、result
文字列は "9876543210" となるはずです。 -
defer
された関数の引数の評価タイミングの検証: 上記のLIFOテストと同時に、引数i
がdefer
ステートメントが呼び出された時点(ループ内でi
がその時点の値を持つ時)で評価され、記憶されることを検証しています。もし引数がdefer
された関数が実際に実行される時(つまりtest1helper
やtest2helper
がリターンする時)に評価されるとすれば、i
はループ終了時の値(10)を持つことになり、テストは失敗するでしょう。しかし、期待される結果 "9876543210" は、引数がdefer
時に評価されることを明確に示しています。test2
では、可変長引数(variadic arguments)を持つ関数addDotDotDot
をdefer
しています。これも同様に、defer
時に引数i
が評価され、単一の整数としてaddDotDotDot
に渡されることを確認しています。可変長引数であっても、defer
の引数評価ルールは一貫していることを示しています。
これらのテストは、Go言語の defer
が設計通りに機能していることを、具体的なコードの実行結果を通じて確認するものです。
コアとなるコードの変更箇所
--- /dev/null
+++ b/test/defer.go
@@ -0,0 +1,52 @@
+// $G $F.go && $L $F.$A && ./$A.out
+
+// Copyright 2009 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package main
+
+import "fmt"
+
+var result string
+
+func addInt(i int) {
+ result += fmt.Sprint(i)
+}
+
+func test1helper() {
+ for i := 0; i < 10; i++ {
+ defer addInt(i)
+ }
+}
+
+func test1() {
+ result = "";
+ test1helper();
+ if result != "9876543210" {
+ fmt.Printf("test1: bad defer result (should be 9876543210): %q\n", result);
+ }
+}
+
+func addDotDotDot(v ...) {
+ result += fmt.Sprint(v)
+}
+
+func test2helper() {
+ for i := 0; i < 10; i++ {
+ defer addDotDotDot(i)
+ }
+}
+
+func test2() {
+ result = "";
+ test2helper();
+ if result != "9876543210" {
+ fmt.Printf("test2: bad defer result (should be 9876543210): %q\n", result);
+ }
+}
+
+func main() {
+ test1();
+ test2();
+}
コアとなるコードの解説
test/defer.go
ファイルは、Go言語の defer
ステートメントの動作を検証するための自己完結型のテストプログラムです。
package main
: 実行可能なプログラムであることを示します。import "fmt"
: 文字列フォーマットと出力のためにfmt
パッケージをインポートします。var result string
: グローバル変数result
を宣言します。これはdefer
された関数が実行された結果を蓄積するために使用されます。func addInt(i int)
: 整数i
をresult
文字列に追加するヘルパー関数です。func test1helper()
:for
ループ内でi
が0
から9
まで増加しながらdefer addInt(i)
を呼び出します。defer
のLIFO特性により、addInt(9)
が最初に実行され、次にaddInt(8)
、...、最後にaddInt(0)
が実行されることを期待します。- また、
addInt
の引数i
はdefer
が呼び出された時点のi
の値(0, 1, ..., 9)で評価され、記憶されます。
func test1()
:result
を初期化し、test1helper()
を呼び出します。test1helper()
がリターンした後、result
の値が "9876543210" であることを検証します。もし異なる場合はエラーメッセージを出力します。
func addDotDotDot(v ...)
: 可変長引数を受け取るヘルパー関数です。受け取った引数v
をresult
文字列に追加します。fmt.Sprint
はスライスを文字列に変換する際に角括弧を含めるため、このテストでは単一の引数として渡されることを期待しています。func test2helper()
:test1helper
と同様に、for
ループ内でdefer addDotDotDot(i)
を呼び出します。test1helper
と同じロジックで、defer
のLIFO特性と引数評価タイミングを可変長引数を持つ関数で検証します。
func test2()
:result
を初期化し、test2helper()
を呼び出します。test2helper()
がリターンした後、result
の値が "9876543210" であることを検証します。
func main()
:- プログラムのエントリポイントです。
test1()
とtest2()
を順に呼び出し、defer
のテストを実行します。
このコードは、Go言語の defer
が期待通りに動作することを確認するための、シンプルかつ効果的なテストケースを提供しています。
関連リンク
- Go言語の
defer
ステートメントに関する公式ドキュメント: - Goブログ: Defer, Panic, and Recover:
参考にした情報源リンク
- Go Language Specification
- A Tour of Go
- Go blog