[インデックス 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
の主な特徴と動作は以下の通りです。
- 実行タイミング:
defer
された関数は、それを囲む関数が実行を終了する直前(return
ステートメントの実行後、またはパニック発生後)に実行されます。 - LIFO順序: 複数の
defer
ステートメントが同じ関数内で宣言された場合、それらはLIFO(Last-In, First-Out)の順序で実行されます。つまり、最後にdefer
された関数が最初に実行され、最初にdefer
された関数が最後に実行されます。 - 引数の評価:
defer
ステートメントの引数は、defer
ステートメントが評価された時点(つまり、defer
が宣言された時点)で評価されます。関数が実際に実行される時点ではありません。これは、defer
の一般的な落とし穴の一つです。 - 一般的な用途:
- リソースの解放: ファイルのクローズ、データベース接続のクローズ、ミューテックスのアンロックなど、取得したリソースを確実に解放するために使用されます。
- トレース/ロギング: 関数の開始と終了をログに記録するために使用できます。
- パニックからの回復:
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
プログラムを実行し、その出力が期待される文字列と一致するかどうかを検証します。
具体的には、以下の変更が行われています。
-
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()) }
- 既存の
-
doc/progs/run
の変更:testit defer "" "0 3210 2"
:- これは
defer.go
プログラムを実行し、標準入力は与えません。 - 期待される標準出力は
"0 3210 2"
です。 a()
関数はi
をインクリメントし、defer
でfmt.Print(i)
を呼び出します。a()
のi
は0
で初期化され、defer
の引数はdefer
宣言時に評価されるため、0
が出力されます。b()
関数はループ内でdefer fmt.Print(i)
を呼び出します。i
は3
から0
までデクリメントされるため、3210
が出力されます(LIFO順)。c()
関数はdefer func() { i++ }()
を持ち、return 1
します。defer
された無名関数はreturn
後に実行されるため、i
は1
から2
にインクリメントされ、最終的に2
がfmt.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.go
のmain
関数
追加されたmain
関数は、defer.go
ファイル内のdefer
の挙動を示す3つの主要な関数(a
, b
, c
)を順に呼び出します。
a()
: この関数は、i
を0
で初期化し、defer fmt.Print(i)
を呼び出した後、i
をインクリメントしてreturn
します。defer
の引数はdefer
が宣言された時点で評価されるため、fmt.Print(i)
は0
を出力します。b()
: この関数はループ内でdefer fmt.Print(i)
を呼び出します。ループはi
が3
から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
ステートメントが実行された後、関数が終了する直前に実行されるため、i
は1
から2
にインクリメントされます。main
関数でfmt.Println(c())
が呼び出されると、最終的なi
の値である2
が出力されます。
これらの呼び出しにより、defer
の引数評価のタイミング、LIFO順序、そして名前付き戻り値とdefer
の相互作用という、defer
の重要な側面がまとめてテストされます。
doc/progs/run
のtestit
エントリ
doc/progs/run
スクリプトに追加されたtestit
エントリは、defer.go
プログラムの実行結果を検証します。
-
testit defer "" "0 3210 2"
:- これは、上記の
main
関数が実行された際の期待される標準出力です。 a()
からの0
、b()
からの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
参考にした情報源リンク
- A Tour of Go: Defer: https://go.dev/tour/flowcontrol/12
- Go by Example: Defer: https://gobyexample.com/defer
- Effective Go: Defer: https://go.dev/doc/effective_go#defer
- The Go Programming Language Specification: Defer statements: https://go.dev/ref/spec#Defer_statements