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

[インデックス 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 の主な特徴は以下の通りです。

  1. 実行タイミング: defer された関数は、それを囲む関数が return する直前、またはパニックによって終了する直前に実行されます。
  2. LIFO (Last-In, First-Out) 順序: 同じ関数内で複数の defer ステートメントが呼び出された場合、それらはスタックのようにLIFOの順序で実行されます。つまり、最後に defer された関数が最初に実行され、最初に defer された関数が最後に実行されます。
  3. 引数の評価タイミング: 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つの主要な動作をテストしています。

  1. LIFO (Last-In, First-Out) 実行順序の検証: test1helper 関数と test2helper 関数内でループを使って複数の defer ステートメントを呼び出しています。 for i := 0; i < 10; i++ { defer addInt(i) } このループでは、i0 から 9 まで増加しながら defer addInt(i) が呼び出されます。 defer のLIFO特性により、addInt(9) が最初に実行され、次に addInt(8)、...、最後に addInt(0) が実行されることを期待しています。 したがって、result 文字列は "9876543210" となるはずです。

  2. defer された関数の引数の評価タイミングの検証: 上記のLIFOテストと同時に、引数 idefer ステートメントが呼び出された時点(ループ内で i がその時点の値を持つ時)で評価され、記憶されることを検証しています。もし引数が defer された関数が実際に実行される時(つまり test1helpertest2helper がリターンする時)に評価されるとすれば、i はループ終了時の値(10)を持つことになり、テストは失敗するでしょう。しかし、期待される結果 "9876543210" は、引数が defer 時に評価されることを明確に示しています。

    test2 では、可変長引数(variadic arguments)を持つ関数 addDotDotDotdefer しています。これも同様に、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): 整数 iresult 文字列に追加するヘルパー関数です。
  • func test1helper():
    • for ループ内で i0 から 9 まで増加しながら defer addInt(i) を呼び出します。
    • defer のLIFO特性により、addInt(9) が最初に実行され、次に addInt(8)、...、最後に addInt(0) が実行されることを期待します。
    • また、addInt の引数 idefer が呼び出された時点の i の値(0, 1, ..., 9)で評価され、記憶されます。
  • func test1():
    • result を初期化し、test1helper() を呼び出します。
    • test1helper() がリターンした後、result の値が "9876543210" であることを検証します。もし異なる場合はエラーメッセージを出力します。
  • func addDotDotDot(v ...): 可変長引数を受け取るヘルパー関数です。受け取った引数 vresult 文字列に追加します。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 Language Specification
  • A Tour of Go
  • Go blog