[インデックス 10703] ファイルの概要
このコミットは、Go言語のドキュメンタリゼーションに「Defer, Panic, and Recover」と題された新しい記事を追加するものです。この記事は、Goプログラミング言語ブログで2010年8月4日に公開されたものを基にしており、Goにおけるdefer、panic、recoverという3つの重要な制御フローメカニズムについて詳細に解説しています。具体的には、これらの機能の動作原理、使用例、およびベストプラクティスが説明されています。
コミット
- コミットハッシュ: 49d82b4ca1a902f5667e845e82440c83287ee633
- 作者: Andrew Gerrand adg@golang.org
- 日付: Mon Dec 12 13:15:29 2011 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/49d82b4ca1a902f5667e845e82440c83287ee633
元コミット内容
doc: add Defer, Panic, and Recover article
Originally published on The Go Programming Language Blog, August 4 2010.
http://blog.golang.org/2010/08/defer-panic-and-recover.html
Update #2547
R=golang-dev, r, r
CC=golang-dev
https://golang.org/cl/5479053
変更の背景
このコミットの背景には、Go言語の公式ドキュメントを充実させ、特にdefer、panic、recoverといったGo特有の強力な制御フローメカニズムについて、開発者により深く理解してもらうという目的があります。これらの機能は、他のプログラミング言語にはあまり見られない独特なものであり、リソースのクリーンアップ、エラーハンドリング、予期せぬ実行時エラーからの回復において非常に有用です。
元々Goブログで公開されていた記事を公式ドキュメントに組み込むことで、情報の集約とアクセシビリティの向上が図られています。これにより、Go言語を学ぶ開発者が、これらの重要な概念をより容易に参照し、実践に役立てることができるようになります。
前提知識の解説
このコミットで追加された記事を理解するためには、Go言語の基本的な制御フロー(if、for、switchなど)と、Goルーチン(goステートメント)に関する知識が前提となります。加えて、以下のGo言語特有の概念について理解を深める必要があります。
defer (遅延実行)
deferステートメントは、そのステートメントを含む関数がリターンする直前に、指定された関数呼び出しを遅延実行させるためのものです。これは、リソースの解放(ファイルクローズ、ロック解除など)や、関数の終了時に必ず実行したいクリーンアップ処理を記述するのに非常に便利です。deferされた関数は、関数の正常終了時だけでなく、panicが発生して関数が異常終了する場合でも実行されます。
panic (パニック)
panicは、Goプログラムの通常の制御フローを停止させる組み込み関数です。panicが呼び出されると、現在の関数の実行が直ちに停止し、その関数内でdeferされた関数がすべて実行されます。その後、panicは呼び出し元関数に伝播し、コールスタックを遡っていきます。もしpanicがGoルーチンの最上位に到達した場合、プログラムはクラッシュします。panicは、回復不能なエラーや、プログラムの続行が不可能な状況を示すために使用されます。
recover (回復)
recoverは、panicが発生したGoルーチンを回復させるための組み込み関数です。recoverはdeferされた関数内でのみ有効です。通常の実行時にはnilを返しますが、panicが発生している最中に呼び出されると、panicに渡された値(エラー情報など)を捕捉し、Goルーチンの通常の実行を再開させます。これにより、プログラム全体のクラッシュを防ぎ、エラーを適切に処理する機会を提供します。
これらの3つの機能は密接に関連しており、特にdeferとrecoverはpanicからの回復メカニズムを構築する上で不可欠です。
技術的詳細
追加された記事は、defer、panic、recoverの動作について以下の3つのシンプルなルールを提示し、詳細な技術的側面を解説しています。
deferの動作ルール
-
遅延関数の引数は、
deferステートメントが評価された時点で評価される。 これは、deferの対象となる関数に渡される引数の値が、deferステートメントが書かれた時点での値に固定されることを意味します。関数が実際に実行されるのは、外側の関数がリターンする直前ですが、引数の評価はその時点ではありません。例:
func a() { i := 0 defer fmt.Println(i) // ここで i は 0 として評価される i++ return // 関数がリターンする際に "0" が出力される } -
遅延関数呼び出しは、外側の関数がリターンした後に、LIFO(Last In First Out)順で実行される。 複数の
deferステートメントがある場合、最後にdeferされた関数が最初に実行され、最初にdeferされた関数が最後に実行されます。例:
func b() { for i := 0; i < 4; i++ { defer fmt.Print(i) // 0, 1, 2, 3 の順で defer される } // 関数がリターンする際に "3210" が出力される } -
遅延関数は、リターンする関数の名前付き戻り値を読み書きできる。 これは、
deferされた関数が、外側の関数の戻り値を変更できることを意味します。これは、エラーハンドリングや戻り値の最終調整に非常に強力な機能です。例:
func c() (i int) { defer func() { i++ }() // i は戻り値として定義されており、defer 関数内で変更可能 return 1 // 関数がリターンする際に i は 1 となるが、defer 関数でインクリメントされ 2 となる }
panicとrecoverの連携
記事では、panicとrecoverがどのように連携して、Goルーチンレベルでのエラー回復メカニズムを提供するかが詳細に説明されています。panicが発生すると、コールスタックを遡りながらdeferされた関数が実行されます。このdeferされた関数内でrecoverが呼び出されると、panicの伝播が停止し、通常の実行が再開されます。
このメカニズムは、Go標準ライブラリのencoding/jsonパッケージなどで実際に使用されています。JSONのデコード中に不正なデータが検出された場合、panicを発生させてスタックを巻き戻し、トップレベルの関数でrecoverして適切なエラー値を返す、といった処理が行われます。Goの慣習として、パッケージが内部でpanicを使用する場合でも、外部APIは明示的なエラー戻り値を返すように設計されています。
コアとなるコードの変更箇所
このコミットでは、主に以下のファイルが変更または新規追加されています。
doc/Makefile: 新しい記事articles/defer_panic_recover.htmlをビルドターゲットに追加。doc/articles/defer_panic_recover.html: 「Defer, Panic, and Recover」記事の最終的なHTML出力ファイル。doc/articles/defer_panic_recover.tmpl: 上記HTMLファイルの生成に使用されるテンプレートファイル。Goのテンプレート構文が含まれています。doc/makehtml: HTML生成スクリプトの修正。出力ファイルパスの処理が改善されています。doc/progs/defer.go:deferの動作ルール(引数の評価タイミング、LIFO順、戻り値の変更)を示すGoコードスニペットが含まれています。また、deferを使用しないCopyFile関数の初期バージョンも含まれます。doc/progs/defer2.go:deferを使用したCopyFile関数の改善版と、panicとrecoverの動作を示すGoコードスニペットが含まれています。doc/progs/run:doc/progsディレクトリ内のGoプログラムをビルドするためのスクリプト。新しいdefer.goとdefer2.goがビルド対象に追加されています。doc/tmpltohtml.go: テンプレートからHTMLを生成するGoプログラムの修正。filepath.Baseを使用してテンプレート名を適切に処理するように変更されています。
コアとなるコードの解説
このコミットの核となるのは、doc/articles/defer_panic_recover.htmlとdoc/articles/defer_panic_recover.tmplで提供される記事の内容、およびそれを補完するdoc/progs/defer.goとdoc/progs/defer2.go内のGoコード例です。
CopyFile関数の改善例 (defer.go vs defer2.go)
記事では、deferの最も一般的なユースケースとして、リソースのクリーンアップが挙げられています。
deferを使用しない初期バージョン (doc/progs/defer.goより抜粋):
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close() // ここでクローズ
src.Close() // ここでクローズ
return
}
このコードにはバグがあり、os.Create(dstName)が失敗した場合、srcファイルが閉じられずにリソースリークが発生する可能性があります。
deferを使用した改善バージョン (doc/progs/defer2.goより抜粋):
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close() // defer を使用して確実にクローズ
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close() // defer を使用して確実にクローズ
return io.Copy(dst, src)
}
deferを使用することで、os.Openやos.Createの直後にClose()を記述でき、関数のどのパスを通ってもファイルが確実に閉じられることが保証されます。これにより、コードの可読性と堅牢性が向上します。
panicとrecoverの動作例 (doc/progs/defer2.goより抜粋)
package main
import "fmt"
func main() {
f()
fmt.Println("Returned normally from f.")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered in f", r)
}
}()
fmt.Println("Calling g.")
g(0)
fmt.Println("Returned normally from g.")
}
func g(i int) {
if i > 3 {
fmt.Println("Panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("Defer in g", i)
fmt.Println("Printing in g", i)
g(i + 1)
}
この例では、g関数が再帰的に呼び出され、iが3を超えるとpanicを発生させます。f関数では、deferされた無名関数内でrecover()が呼び出されています。
g(0)から始まり、g(1)、g(2)、g(3)と再帰呼び出しが続きます。g(4)が呼び出されると、i > 3の条件が真となり、Panicking!が出力され、panic("4")が呼び出されます。panicが発生すると、g(3)、g(2)、g(1)、g(0)の順でdeferされた関数が実行されます(LIFO順)。- その後、
panicはf関数に伝播し、f関数内でdeferされた関数が実行されます。 - この
deferされた関数内でrecover()が呼び出され、panicの値("4")を捕捉し、Recovered in f 4が出力されます。 recoverによりpanicが回復されたため、f関数は正常に終了し、Returned normally from f.が出力されます。
この例は、panicがどのようにコールスタックを遡るか、deferがどのようにその過程で実行されるか、そしてrecoverがどのようにpanicを捕捉してプログラムのクラッシュを防ぐかを示しています。
関連リンク
- Go Programming Language Blog: Defer, Panic, and Recover
- Go CL (Change List): https://golang.org/cl/5479053
参考にした情報源リンク
- Go Programming Language Blog: Defer, Panic, and Recover
- Go言語公式ドキュメント (defer): https://go.dev/ref/spec#Defer_statements
- Go言語公式ドキュメント (panic): https://go.dev/ref/spec#Handling_panics
- Go言語公式ドキュメント (recover): https://go.dev/ref/spec#Handling_panics
- Go言語の
encoding/jsonパッケージのソースコード (特にdecode.go): https://cs.opensource.google/go/go/+/refs/tags/go1.22.4:src/encoding/json/decode.go (これは一般的な参照であり、コミット時点の正確なリンクではない可能性がありますが、概念を理解する上で有用です。)