[インデックス 12304] ファイルの概要
このコミットは、Go言語のテストスイートに、可変長引数(varargs)関数内でrecover
を呼び出すケースのテストを追加するものです。特に、gccgo
コンパイラがこのシナリオを正しく処理していなかった問題に対処するために導入されました。これにより、panic
が発生した際に可変長引数関数内のdefer
でrecover
が期待通りに動作するかを確認します。
コミット
commit b14a6643dc47104689facd938a0fb254996ddf85
Author: Ian Lance Taylor <iant@golang.org>
Date: Thu Mar 1 08:24:03 2012 -0800
test: add test of calling recover in a varargs function
gccgo did not handle this correctly.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5714050
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b14a6643dc47104689facd938a0fb254996ddf85
元コミット内容
test: add test of calling recover in a varargs function
gccgo did not handle this correctly.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5714050
変更の背景
この変更の主な背景は、Go言語のコンパイラの一つであるgccgo
が、可変長引数関数(variadic function)内でrecover
関数が呼び出された際に、その動作を正しく処理できていなかったというバグが存在したことです。
Go言語には、プログラムの異常終了を防ぐためのpanic
とrecover
というメカニズムがあります。panic
は実行時のエラーや予期せぬ状況が発生した際にプログラムの通常のフローを中断させるもので、recover
はdefer
文の中で呼び出されることで、panic
によって中断されたパニックシーケンスを捕捉し、プログラムの制御を回復させるために使用されます。
可変長引数関数は、引数の数が不定である関数を定義できるGoの強力な機能です。gccgo
は、Go言語のフロントエンドとしてGCC(GNU Compiler Collection)のバックエンドを利用するコンパイラであり、標準のGoコンパイラ(gc
)とは異なる実装を持っています。この実装の違いが、特定の複雑なシナリオ、特に可変長引数関数のスタックフレームや引数処理とpanic
/recover
メカニズムの相互作用において、バグを引き起こしていたと考えられます。
このコミットは、gccgo
のこのバグを特定し、修正を検証するために、具体的なテストケースをGoの標準テストスイートに追加することを目的としています。テストを追加することで、将来的に同様の回帰バグが発生することを防ぎ、gccgo
を含むGoコンパイラの実装がpanic
/recover
のセマンティクスを正しく遵守していることを保証します。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念を理解しておく必要があります。
-
panic
とrecover
:panic
: Go言語におけるランタイムエラーや予期せぬ状況が発生した際に、プログラムの通常の実行フローを中断させるメカニズムです。panic
が呼び出されると、現在の関数の実行が停止し、その関数にdefer
された関数が実行されます。その後、呼び出し元の関数へとパニックが伝播し、同様にdefer
された関数が実行され、最終的にプログラムがクラッシュします。recover
:panic
によって中断されたパニックシーケンスを捕捉し、プログラムの制御を回復させるために使用される組み込み関数です。recover
は**defer
された関数の中でのみ**有効に機能します。recover
がdefer
された関数内で呼び出されると、パニックの値(panic
に渡された引数)が返され、パニックシーケンスは停止し、プログラムはrecover
を呼び出したdefer
文を含む関数の次のステートメントから通常の実行を再開します。defer
されていない場所でrecover
を呼び出してもnil
が返され、効果はありません。
-
defer
文:defer
文は、そのdefer
文を含む関数がリターンする直前(panic
が発生した場合も含む)に、指定された関数呼び出しを実行することを保証します。複数のdefer
文がある場合、それらはLIFO(Last-In, First-Out)の順序で実行されます。panic
が発生した場合、defer
された関数は、スタックをアンワインドする過程で実行されます。
-
可変長引数関数(Variadic Functions):
- Go言語では、引数の数が不定である関数を定義できます。これは、最後のパラメータの型名の前に
...
を付けることで実現されます(例:func sum(nums ...int)
)。関数内で可変長引数はスライスとして扱われます。例えば、nums ...int
は関数内では[]int
型のスライスとしてアクセスできます。
- Go言語では、引数の数が不定である関数を定義できます。これは、最後のパラメータの型名の前に
-
gccgo
とgc
:gc
: これはGo言語の公式かつ標準のコンパイラであり、Goのソースコードをネイティブバイナリにコンパイルします。ほとんどのGo開発者が日常的に使用しているコンパイラです。gccgo
: これはGCC(GNU Compiler Collection)のフロントエンドとして実装されたGoコンパイラです。gc
とは異なるコード生成バックエンドを使用しており、GCCがサポートする様々なアーキテクチャや最適化を利用できるという利点があります。しかし、異なる実装であるため、gc
とは異なるバグや挙動の違いが発生することがあります。このコミットで修正された問題は、まさにgccgo
特有のバグでした。
これらの概念がどのように相互作用するか、特にpanic
が可変長引数関数内で発生し、その関数がdefer
され、さらにそのdefer
された関数内でrecover
が呼び出されるという複雑なシナリオが、このテストの焦点となっています。
技術的詳細
このコミットが追加するテストケースは、recover
が可変長引数関数内で正しく機能するかどうかを検証します。具体的には、defer
された可変長引数関数内でrecover
が呼び出された場合の挙動に焦点を当てています。
Go言語のpanic
/recover
メカニズムは、スタックのアンワインド(unwinding)と密接に関連しています。panic
が発生すると、現在の関数の実行が中断され、defer
された関数が実行されながら、呼び出しスタックを逆順に辿っていきます。この過程でrecover
が呼び出されると、パニックが捕捉され、スタックのアンワインドが停止し、プログラムの制御が回復します。
可変長引数関数は、その引数がスライスとして扱われるため、通常の固定引数関数とは異なる方法でスタックフレームが構築される可能性があります。特に、引数がスタック上にどのように配置され、defer
された関数がそのスタックフレームにどのようにアクセスするかは、コンパイラの実装に依存します。
gccgo
における問題は、おそらく以下のいずれかのシナリオに関連していたと考えられます。
- スタックフレームの不整合:
gccgo
が可変長引数関数のスタックフレームを構築する際に、panic
発生時のスタックアンワインド処理やrecover
が期待するスタック情報との間に不整合があった可能性があります。これにより、recover
がパニック値を正しく取得できなかったり、パニックシーケンスを適切に停止できなかったりしたかもしれません。 - 引数スライスのライフタイム: 可変長引数スライスは、関数が呼び出されたときに作成されます。
panic
が発生し、defer
された関数が実行される際に、このスライスのデータがまだ有効であるか、またはrecover
がそのデータに正しくアクセスできるかどうかに問題があった可能性も考えられます。 - レジスタとスタックの不一致: コンパイラはパフォーマンスのために引数をレジスタに配置することがありますが、
panic
発生時にはスタックに退避された情報が使用されることがあります。gccgo
が可変長引数関数において、レジスタとスタックの間で引数の状態を正しく同期できていなかった可能性も考えられます。
このテストは、varargs
という可変長引数関数を定義し、その中でrecover()
を呼び出しています。そして、test8a
関数ではpanic(0)
を発生させ、test8b
関数では通常のreturn
を行います。どちらの関数もdefer varargs(...)
を使ってvarargs
関数を遅延実行しています。
test8a
のケースでは、panic
が発生するため、defer
されたvarargs
関数が実行され、その中でrecover()
がnil
ではない値を返すことを期待します。これにより、*s += 100
が実行され、r
の値が100 + (1+2+3) = 106
になることを検証します。test8b
のケースでは、panic
は発生しないため、defer
されたvarargs
関数内でrecover()
はnil
を返します。これにより、*s += 100
は実行されず、r
の値が4+5+6 = 15
になることを検証します。
これらのテストケースを通じて、gccgo
がpanic
とrecover
、そして可変長引数関数の組み合わせを正しく処理できるようになったことを確認します。
コアとなるコードの変更箇所
変更はtest/recover.go
ファイルに対して行われています。
--- a/test/recover.go
+++ b/test/recover.go
@@ -244,3 +244,30 @@ func test7() {
die()
}
}
+
+func varargs(s *int, a ...int) {
+ *s = 0
+ for _, v := range a {
+ *s += v
+ }
+ if recover() != nil {
+ *s += 100
+ }
+}
+
+func test8a() (r int) {
+ defer varargs(&r, 1, 2, 3)
+ panic(0)
+}
+
+func test8b() (r int) {
+ defer varargs(&r, 4, 5, 6)
+ return
+}
+
+func test8() {
+ if test8a() != 106 || test8b() != 15 {
+ println("wrong value")
+ die()
+ }
+}
具体的には、以下の新しい関数が追加されています。
varargs(s *int, a ...int)
test8a() (r int)
test8b() (r int)
test8()
コアとなるコードの解説
追加された各関数の役割は以下の通りです。
-
func varargs(s *int, a ...int)
:- この関数は可変長引数
a ...int
を受け取ります。これは関数内で[]int
型のスライスとして扱われます。 s *int
は、結果を格納するためのポインタです。- 関数内で、まず
*s
を0
に初期化し、次に可変長引数a
の要素をすべて合計して*s
に加算します。 - 最も重要なのは、
if recover() != nil
のブロックです。ここでrecover()
が呼び出されます。- もしこの
varargs
関数がpanic
によってdefer
経由で呼び出された場合、recover()
はnil
ではないパニック値を返します。この場合、*s
に100
が加算されます。 - もし
panic
が発生せずに通常の関数終了によってdefer
経由で呼び出された場合、recover()
はnil
を返します。この場合、*s
に100
は加算されません。
- もしこの
- この関数は、
panic
の有無によって*s
の値が変化することを利用して、recover
の動作をテストします。
- この関数は可変長引数
-
func test8a() (r int)
:- この関数は
r int
という名前付き戻り値を持ちます。 defer varargs(&r, 1, 2, 3)
: この行は、test8a
関数が終了する直前にvarargs
関数を遅延実行するように設定します。varargs
にはr
のアドレスと、引数1, 2, 3
が渡されます。panic(0)
: この行で意図的にpanic
を発生させます。これにより、test8a
の実行は中断され、defer
されたvarargs
関数が実行されます。panic
が発生するため、varargs
関数内のrecover()
はパニックを捕捉し、r
には1 + 2 + 3 + 100 = 106
が設定されることを期待します。
- この関数は
-
func test8b() (r int)
:- この関数も
r int
という名前付き戻り値を持ちます。 defer varargs(&r, 4, 5, 6)
:test8b
関数が終了する直前にvarargs
関数を遅延実行するように設定します。varargs
にはr
のアドレスと、引数4, 5, 6
が渡されます。return
: この行で関数は正常に終了します。panic
は発生しません。panic
が発生しないため、varargs
関数内のrecover()
はnil
を返し、r
には4 + 5 + 6 = 15
が設定されることを期待します。
- この関数も
-
func test8()
:- この関数は、
test8a()
とtest8b()
を呼び出し、それぞれの戻り値が期待通りであるかを検証します。 if test8a() != 106 || test8b() != 15
: もしtest8a()
が106
ではないか、またはtest8b()
が15
ではない場合、テストは失敗と判断されます。println("wrong value")
とdie()
: テストが失敗した場合にメッセージを出力し、プログラムを終了させます。
- この関数は、
これらの関数が連携することで、可変長引数関数内でrecover
が呼び出された際のpanic
の捕捉と値の回復が、gccgo
を含むGoコンパイラで正しく行われることを厳密にテストしています。
関連リンク
- Go CL 5714050: https://golang.org/cl/5714050
参考にした情報源リンク
- Go言語の公式ドキュメント:
panic
とrecover
に関するセクション - Go言語の公式ドキュメント:
defer
に関するセクション - Go言語の公式ドキュメント: 可変長引数関数に関するセクション
- GCCGoプロジェクトのドキュメント(一般的な情報源として)
- Go言語のテストスイートの構造と慣習に関する情報(一般的な情報源として)