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

[インデックス 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言語の panicrecover

Go言語における panicrecover は、例外処理に似たメカニズムを提供しますが、その目的は異なります。

  • 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() から返された eruntime.Error でない場合、無条件に e.(error) という型アサーションを行っていました。しかし、前述の通り、panic は任意の型の値を取ることができるため、eerror 型ではない(例えば文字列やカスタム構造体など)場合、この型アサーションはランタイムパニック(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 の実際の型に基づいて異なる処理を行うことができます。

  1. case runtime.Error:: eruntime.Error 型の場合、以前と同様に panic(e) を呼び出して再パニックさせます。これは、ランタイムエラーが通常、回復不能な状態を示すためです。
  2. case error:: eerror 型の場合、その値を *errp に代入します。これにより、テンプレート実行中に発生したエラーを適切に捕捉し、呼び出し元に伝えることができます。
  3. default:: 上記のどの型にも一致しない場合(例えば、panic("some string") のように文字列でパニックした場合)、panic(e) を呼び出して再パニックさせます。これは、text/templateerror 型のパニックのみを捕捉し、それ以外のパニックは予期しないものとして扱うという設計思想に基づいていると考えられます。これにより、予期せぬパニックが隠蔽されることなく、プログラムのクラッシュとして表面化し、デバッグを容易にします。

この修正により、text/templatepanic の値が 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/templatepanic の値が error 型であるという誤った仮定を取り除き、より堅牢なパニックハンドリングを実現しました。

関連リンク

参考にした情報源リンク