[インデックス 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