[インデックス 11061] ファイルの概要
このコミットは、Go言語の標準ライブラリである text/template
パッケージ内の exec.go
ファイルに対する変更です。具体的には、テンプレートの実行中に発生するパニック(panic)のハンドリングロジックを改善し、パニックの値が error
型ではない場合にも適切に対応できるように修正しています。
コミット
- コミットハッシュ:
f5d024a74695510fcb0890807849ec95253a56cd
- 作者: Rémy Oudompheng
- コミット日時: 2012年1月9日 月曜日 12:54:31 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f5d024a74695510fcb0890807849ec95253a56cd
元コミット内容
text/template: handle panic values that are not errors.
The recover code assumes that the panic() argument was
an error, but it is usually a simple string.
Fixes #2663.
R=golang-dev, r, r, gri
CC=golang-dev, remy
https://golang.org/cl/5527046
変更の背景
Go言語では、プログラムの異常終了を示すために panic
関数が使用されます。panic
が呼び出されると、通常の実行フローは中断され、遅延関数(defer
)が実行され、最終的にプログラムがクラッシュします。しかし、recover
関数を defer
関数内で呼び出すことで、パニックからの回復(リカバリ)を試みることができます。
このコミットが行われる前の text/template
パッケージの exec.go
内の errRecover
関数では、recover()
から返される値が常に error
型であると仮定していました。しかし、Go言語の panic
関数は任意の型の値を引数として取ることができ、特に文字列がパニックの値としてよく使われます。この仮定が誤っていたため、error
型ではない値でパニックが発生した場合に、text/template
のリカバリロジックが正しく機能せず、予期せぬクラッシュや動作不良を引き起こす可能性がありました。
この問題は、GoのIssueトラッカーで #2663
として報告されていました。このコミットは、その問題を解決するために、recover
から得られるパニックの値を適切に型アサーションし、error
型でない場合も適切に処理するように修正することを目的としています。
前提知識の解説
Go言語の panic
と recover
Go言語における panic
と recover
は、例外処理に似たメカニズムを提供しますが、その目的は異なります。
panic
: プログラムが回復不可能な状態に陥ったことを示すために使用されます。例えば、配列のインデックスが範囲外になった場合や、nilポインタのデリファレンスなど、ランタイムエラーによって自動的にpanic
が発生することもあります。開発者が明示的にpanic(v interface{})
を呼び出すことも可能です。v
は任意の型の値を取ることができます。defer
:defer
ステートメントは、それが含まれる関数がリターンする直前(panic
が発生した場合も含む)に、指定された関数を実行することを保証します。recover
:defer
関数内でrecover()
を呼び出すと、現在のゴルーチンで発生したパニックを捕捉し、そのパニックの値を返します。recover
がパニック中に呼び出された場合、プログラムの実行は通常のフローに戻ります。パニック中でないときにrecover
を呼び出すと、nil
が返されます。
一般的な panic
/recover
の使用パターンは、defer
関数内で recover
を呼び出し、パニックが発生したかどうかをチェックし、もし発生していれば適切なエラー処理を行うというものです。
func mightPanic() {
// 何らかの処理
panic("Something went wrong!") // 文字列でパニック
}
func main() {
defer func() {
if r := recover(); r != nil {
// r は "Something went wrong!" という文字列になる
fmt.Println("Recovered from panic:", r)
}
}()
mightPanic()
fmt.Println("This line will not be executed if panic occurs and is recovered.")
}
runtime.Error
インターフェース
runtime.Error
は、Goのランタイムが生成するエラーを表すインターフェースです。例えば、ゼロ除算やnilポインタデリファレンスなど、Goランタイムが検出する特定の種類のパニックは、runtime.Error
型の値を伴います。これらのエラーは通常、プログラムのバグを示しており、ほとんどの場合、リカバリすべきではありません。このコミットの変更前も後も、runtime.Error
型のパニックは再パニック(panic(e)
)されることで、プログラムのクラッシュを意図的に継続させています。これは、ランタイムエラーは通常、回復不能な状態を示すためです。
技術的詳細
このコミットの核心は、text/template
パッケージの exec.go
ファイルにある errRecover
関数内の recover
から返される値の型チェックと処理の改善です。
変更前のコードは以下のようになっていました。
func errRecover(errp *error) {
e := recover()
if e != nil {
if _, ok := e.(runtime.Error); ok {
panic(e) // runtime.Error の場合は再パニック
}
*errp = e.(error) // ここで e が error 型であると仮定して型アサーション
}
}
このコードでは、recover()
から返された e
が runtime.Error
でない場合、無条件に e.(error)
という型アサーションを行っていました。しかし、前述の通り、panic
は任意の型の値を取ることができるため、e
が error
型ではない(例えば文字列やカスタム構造体など)場合、この型アサーションはランタイムパニック(interface conversion: interface {} is string, not error
のようなエラー)を引き起こし、元のパニックを捕捉するどころか、新たなパニックでプログラムをクラッシュさせてしまう可能性がありました。
変更後のコードは以下のようになります。
func errRecover(errp *error) {
e := recover()
if e != nil {
switch err := e.(type) { // switch文で e の型をチェック
case runtime.Error:
panic(e) // runtime.Error の場合は再パニック
case error:
*errp = err // error 型の場合は errp に代入
default:
panic(e) // それ以外の型の場合は再パニック
}
}
}
この変更では、if _, ok := e.(runtime.Error); ok
の代わりに switch err := e.(type)
を使用しています。これにより、e
の実際の型に基づいて異なる処理を行うことができます。
case runtime.Error:
:e
がruntime.Error
型の場合、以前と同様にpanic(e)
を呼び出して再パニックさせます。これは、ランタイムエラーが通常、回復不能な状態を示すためです。case error:
:e
がerror
型の場合、その値を*errp
に代入します。これにより、テンプレート実行中に発生したエラーを適切に捕捉し、呼び出し元に伝えることができます。default:
: 上記のどの型にも一致しない場合(例えば、panic("some string")
のように文字列でパニックした場合)、panic(e)
を呼び出して再パニックさせます。これは、text/template
がerror
型のパニックのみを捕捉し、それ以外のパニックは予期しないものとして扱うという設計思想に基づいていると考えられます。これにより、予期せぬパニックが隠蔽されることなく、プログラムのクラッシュとして表面化し、デバッグを容易にします。
この修正により、text/template
は panic
の値が error
型であるという誤った仮定を取り除き、より堅牢なパニックハンドリングを実現しました。
コアとなるコードの変更箇所
変更は src/pkg/text/template/exec.go
ファイルの errRecover
関数内で行われています。
--- a/src/pkg/text/template/exec.go
+++ b/src/pkg/text/template/exec.go
@@ -78,10 +78,14 @@ func (s *state) error(err error) {
func errRecover(errp *error) {
e := recover()
if e != nil {
- if _, ok := e.(runtime.Error); ok {
+ switch err := e.(type) {
+ case runtime.Error:
+ panic(e)
+ case error:
+ *errp = err
+ default:
panic(e)
}
- *errp = e.(error)
}
}
コアとなるコードの解説
errRecover
関数は、text/template
パッケージ内でテンプレートの実行中に発生する可能性のあるパニックを捕捉するために使用される defer
関数です。
e := recover()
: パニックが発生した場合、recover()
はパニックの値を返します。パニックが発生していない場合はnil
を返します。if e != nil
: パニックが発生した場合のみ、以下の処理に進みます。switch err := e.(type)
: ここが変更の核心です。e
の動的な型に基づいて処理を分岐させます。case runtime.Error:
: Goランタイムによって引き起こされたパニック(例: nilポインタデリファレンス)。これらは通常、回復不能なバグを示すため、panic(e)
で再パニックさせ、プログラムをクラッシュさせます。case error:
:error
インターフェースを満たす値でパニックした場合。これは、テンプレートの実行中に意図的にエラーとしてパニックされた場合などに該当します。この場合、*errp = err
によって、パニックの値をerrp
(*error
型のポインタ)が指す変数に代入し、呼び出し元がこのエラーを処理できるようにします。default:
: 上記のいずれにも該当しない場合(例:panic("some string")
)。これはtext/template
が予期しないパニックであるため、panic(e)
で再パニックさせ、プログラムをクラッシュさせます。これにより、予期せぬパニックが隠蔽されることなく、開発者が問題を特定しやすくなります。
この修正により、text/template
は panic
の値が error
型であるという誤った仮定を取り除き、より堅牢なパニックハンドリングを実現しました。
関連リンク
- Go Issue 2663: https://github.com/golang/go/issues/2663
- Go CL 5527046: https://golang.org/cl/5527046
参考にした情報源リンク
- Go言語の
panic
とrecover
に関する公式ドキュメントやチュートリアル- A Tour of Go: https://go.dev/tour/concurrency/12 (Defer, Panic, and Recover)
- Effective Go: https://go.dev/doc/effective_go#recover
- Go言語の型アサーションと型スイッチに関する情報
- A Tour of Go: https://go.dev/tour/methods/15 (Type switches)
runtime.Error
インターフェースに関するGoのドキュメント- GoDoc:
runtime.Error
https://pkg.go.dev/runtime#Error
- GoDoc:
- GitHubのコミット履歴とIssueトラッカー
- Go言語のソースコード (
src/pkg/text/template/exec.go
)