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

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

このコミットは、Go言語のドキュメントに含まれるプログラム例(doc/progs)において、deferキーワードの動作をテストするための新しいプログラムと、そのテスト実行を追加するものです。これにより、deferの挙動が期待通りであることを確認し、ドキュメントの正確性を保証します。

コミット

commit 8f1cb093ff3af8efc426112231e99e887ebe8944
Author: Andrew Gerrand <adg@golang.org>
Date:   Thu Jan 5 16:43:02 2012 +1100

    doc/progs: test defer programs
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/5517044

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

https://github.com/golang/go/commit/8f1cb093ff3af8efc426112231e99e887ebe8944

元コミット内容

doc/progs: test defer programs

このコミットは、Go言語のドキュメントに含まれるプログラム例(doc/progs)に、deferステートメントの動作を検証するためのテストプログラムを追加します。

変更の背景

Go言語のdeferステートメントは、関数の実行が終了する直前に、指定された関数呼び出しを延期して実行する強力な機能です。これはリソースの解放(ファイルクローズ、ロック解除など)やエラーハンドリングにおいて非常に有用ですが、その動作は初心者にとって直感的でない場合があります。

このコミットの背景には、doc/progsディレクトリにある既存のdeferに関するコード例が、実際に期待通りの出力を生成するかどうかを自動的に検証するメカニズムが不足していたという点があります。ドキュメントのコード例は、読者がその言語機能を理解するための重要なリソースであるため、それらが正しく動作することを保証することは非常に重要です。

この変更により、deferの挙動を示す既存のコードスニペットが、実際に正しい出力を生成するかどうかを自動テストで確認できるようになります。これにより、ドキュメントの信頼性が向上し、将来のGo言語の変更によってdeferの動作が変わった場合でも、テストが失敗することで早期に問題を検出できるようになります。

前提知識の解説

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

deferステートメントは、Go言語のユニークな機能の一つで、関数がリターンする直前に実行される関数呼び出しをスケジュールするために使用されます。deferされた関数は、その関数がreturnステートメントによって終了する場合でも、パニック(panic)によって終了する場合でも、必ず実行されます。

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

  1. 実行タイミング: deferされた関数は、それを囲む関数が実行を終了する直前(returnステートメントの実行後、またはパニック発生後)に実行されます。
  2. LIFO順序: 複数のdeferステートメントが同じ関数内で宣言された場合、それらはLIFO(Last-In, First-Out)の順序で実行されます。つまり、最後にdeferされた関数が最初に実行され、最初にdeferされた関数が最後に実行されます。
  3. 引数の評価: deferステートメントの引数は、deferステートメントが評価された時点(つまり、deferが宣言された時点)で評価されます。関数が実際に実行される時点ではありません。これは、deferの一般的な落とし穴の一つです。
  4. 一般的な用途:
    • リソースの解放: ファイルのクローズ、データベース接続のクローズ、ミューテックスのアンロックなど、取得したリソースを確実に解放するために使用されます。
    • トレース/ロギング: 関数の開始と終了をログに記録するために使用できます。
    • パニックからの回復: recover関数と組み合わせて、パニックから回復し、プログラムのクラッシュを防ぐために使用されます。

:

func example() {
    fmt.Println("関数開始")
    defer fmt.Println("defer 1")
    defer fmt.Println("defer 2")
    fmt.Println("関数終了")
}
// 出力:
// 関数開始
// 関数終了
// defer 2
// defer 1

doc/progsディレクトリとrunスクリプト

Go言語のソースコードリポジトリには、doc/progsというディレクトリが存在します。このディレクトリには、Go言語の様々な機能や概念を説明するための短いGoプログラムの例が含まれています。これらのプログラムは、Goのドキュメントやチュートリアルで参照されることがあります。

doc/progs/runは、これらのプログラム例を実行し、その出力を検証するためのシェルスクリプトです。このスクリプトは、testitというヘルパー関数を使用して、特定のGoプログラムを実行し、その標準出力と標準エラー出力を期待される出力と比較します。これにより、ドキュメントに記載されているコード例が、実際にその通りの動作をすることを確認する自動テストの役割を果たしています。

testit関数の基本的な形式は以下の通りです。

testit <program_name> <stdin_input> <expected_stdout> <expected_stderr>
  • <program_name>: doc/progsディレクトリ内のGoプログラムのファイル名(拡張子なし)。
  • <stdin_input>: プログラムに与える標準入力。
  • <expected_stdout>: プログラムの標準出力として期待される文字列。
  • <expected_stderr>: プログラムの標準エラー出力として期待される文字列。

このスクリプトは、GoのビルドシステムやCI/CDパイプラインの一部として実行され、ドキュメントのコード例が常に最新のGo言語の挙動と一致していることを保証します。

技術的詳細

このコミットは、doc/progs/defer.goファイルにmain関数を追加し、既存のdeferの例(a, b, c関数)を呼び出すように変更しています。これにより、これらの関数が実行され、その出力が標準出力に表示されるようになります。

さらに、doc/progs/runスクリプトに新しいtestitエントリが追加されています。これらのエントリは、defer.goプログラムを実行し、その出力が期待される文字列と一致するかどうかを検証します。

具体的には、以下の変更が行われています。

  1. doc/progs/defer.goの変更:

    • 既存のa(), b(), c()関数の定義の間に空行が追加され、コードの可読性が向上しています。
    • ファイルの最後にmain()関数が追加されました。このmain関数は、a(), b(), c()関数を順に呼び出し、b()の後に改行を出力し、c()の戻り値も出力します。これにより、defer.goが単独で実行可能なプログラムとなり、deferの各例の出力をまとめて確認できるようになります。
    func main() {
        a()
        b()
        fmt.Println() // b()の出力とc()の出力を区切る
        fmt.Println(c())
    }
    
  2. doc/progs/runの変更:

    • testit defer "" "0 3210 2":
      • これはdefer.goプログラムを実行し、標準入力は与えません。
      • 期待される標準出力は "0 3210 2" です。
      • a()関数はiをインクリメントし、deferfmt.Print(i)を呼び出します。a()i0で初期化され、deferの引数はdefer宣言時に評価されるため、0が出力されます。
      • b()関数はループ内でdefer fmt.Print(i)を呼び出します。i3から0までデクリメントされるため、3210が出力されます(LIFO順)。
      • c()関数はdefer func() { i++ }()を持ち、return 1します。deferされた無名関数はreturn後に実行されるため、i1から2にインクリメントされ、最終的に2fmt.Println(c())によって出力されます。
      • したがって、全体の出力は0 3210\n2となります。
    • testit defer2 "" "Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f.":
      • これは、defer.goのコメントアウトされた部分(おそらく以前のバージョンか、別の例)に関連するテストエントリです。このコミットの差分にはdefer2というプログラムは含まれていませんが、runスクリプトに追加されていることから、defer.goの以前のバージョンや、このコミットとは別の変更でdefer2というプログラムが追加されたか、あるいは将来追加されることを想定している可能性があります。このテストは、パニックと回復を含むdeferのより複雑なシナリオを検証していると推測されます。

これらの変更により、deferの基本的な動作(引数の評価タイミング、LIFO順序、return後の実行)が自動的に検証されるようになり、ドキュメントのコード例の正確性が保証されます。

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

doc/progs/defer.go

--- a/doc/progs/defer.go
+++ b/doc/progs/defer.go
@@ -18,6 +18,7 @@ func a() {
 	i++
 	return
 }
+
 // STOP OMIT
 
 func b() {
@@ -25,12 +26,14 @@ func b() {
 		defer fmt.Print(i)
 	}
 }
+
 // STOP OMIT
 
 func c() (i int) {
 	defer func() { i++ }()
 	return 1
 }
+
 // STOP OMIT
 
 // Intial version.
@@ -50,4 +53,12 @@ func CopyFile(dstName, srcName string) (written int64, err error) {
 	src.Close()
 	return
 }
+
 // STOP OMIT
+
+func main() {
+	a()
+	b()
+	fmt.Println()
+	fmt.Println(c())
+}

doc/progs/run

--- a/doc/progs/run
+++ b/doc/progs/run
@@ -95,6 +95,8 @@ testit helloworld3 "" "hello, world can't open file; err=no such file or directo
 testit echo "hello, world" "hello, world"
 testit sum "" "6"
 testit strings "" ""
+testit defer "" "0 3210 2"
+testit defer2 "" "Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f."
 
 alphabet=abcdefghijklmnopqrstuvwxyz
 rot13=nopqrstuvwxyzabcdefghijklm

コアとなるコードの解説

doc/progs/defer.gomain関数

追加されたmain関数は、defer.goファイル内のdeferの挙動を示す3つの主要な関数(a, b, c)を順に呼び出します。

  • a(): この関数は、i0で初期化し、defer fmt.Print(i)を呼び出した後、iをインクリメントしてreturnします。deferの引数はdeferが宣言された時点で評価されるため、fmt.Print(i)0を出力します。
  • b(): この関数はループ内でdefer fmt.Print(i)を呼び出します。ループはi3から0までデクリメントされるため、deferされた関数はfmt.Print(3), fmt.Print(2), fmt.Print(1), fmt.Print(0)の順にスタックに積まれます。実行時にはLIFO順で3210と出力されます。
  • c(): この関数は名前付き戻り値iを持ち、defer func() { i++ }()を呼び出した後、return 1します。deferされた無名関数はreturnステートメントが実行された後、関数が終了する直前に実行されるため、i1から2にインクリメントされます。main関数でfmt.Println(c())が呼び出されると、最終的なiの値である2が出力されます。

これらの呼び出しにより、deferの引数評価のタイミング、LIFO順序、そして名前付き戻り値とdeferの相互作用という、deferの重要な側面がまとめてテストされます。

doc/progs/runtestitエントリ

doc/progs/runスクリプトに追加されたtestitエントリは、defer.goプログラムの実行結果を検証します。

  • testit defer "" "0 3210 2":

    • これは、上記のmain関数が実行された際の期待される標準出力です。
    • a()からの0b()からの3210、そしてc()からの2が、それぞれスペース区切りで出力され、最後に改行が入り、その後にc()の戻り値である2が改行されて出力されることを期待しています。
    • このテストは、deferの基本的な動作がGo言語の仕様通りであることを確認します。
  • testit defer2 "" "Calling g. Printing in g 0 Printing in g 1 Printing in g 2 Printing in g 3 Panicking! Defer in g 3 Defer in g 2 Defer in g 1 Defer in g 0 Recovered in f 4 Returned normally from f.":

    • このエントリは、defer2という別のプログラム(このコミットの差分には含まれていないが、おそらく関連するテストプログラム)のテストです。
    • 期待される出力から、このdefer2プログラムは、パニックが発生した場合のdeferの実行順序と、recover関数によるパニックからの回復の挙動をテストしていることがわかります。パニックが発生してもdefer関数は実行され、recoverによってプログラムが正常に続行できることを示しています。

これらのテストエントリは、Go言語のdeferステートメントの堅牢性と、様々なシナリオ(正常終了、パニック)におけるその予測可能な動作を保証するために不可欠です。

関連リンク

  • Go Code Review Comments: https://golang.org/cl/5517044

参考にした情報源リンク