[インデックス 19455] ファイルの概要
このコミットは、Goランタイムにおけるnil
関数値をgo
キーワードで呼び出した際の挙動を修正するものです。具体的には、go f()
のようにf
がnil
である場合に、以前は回復不能な致命的エラー(fatal error)を引き起こしていた問題を、パニック(panic)を発生させるように変更しています。これにより、プログラムがより予測可能な形で異常終了し、recover
メカニズムによる捕捉の可能性も生まれます。
変更が加えられたファイルは以下の通りです。
src/pkg/runtime/crash_test.go
:go nil
関数呼び出しの新しいテストケースが追加されました。このテストは、nil
関数値のgo
呼び出しが期待通りにパニックを発生させることを検証します。src/pkg/runtime/proc.c
: Goランタイムのプロシージャ(goroutineの生成など)を扱うC言語のソースファイルです。runtime·newproc1
関数にnil
関数値のチェックが追加され、パニックを発生させるロジックが導入されました。
コミット
commit b5caa02067e0e0d2bde9290004b15e9a226c6075
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Wed May 28 00:00:01 2014 -0400
runtime: fix go of nil func value
Currently runtime derefences nil with m->locks>0,
which causes unrecoverable fatal error.
Panic instead.
Fixes #8045.
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews, khr
https://golang.org/cl/97620043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b5caa02067e0e0d2bde9290004b15e9a226c6075
元コミット内容
runtime: fix go of nil func value
Currently runtime derefences nil with m->locks>0,
which causes unrecoverable fatal error.
Panic instead.
Fixes #8045.
LGTM=rsc
R=golang-codereviews, rsc
CC=golang-codereviews, khr
https://golang.org/cl/97620043
変更の背景
この変更の背景には、Goプログラムがnil
関数値をgo
ステートメントで新しいゴルーチンとして起動しようとした際に発生していた、回復不能な致命的エラーの問題があります。
Go言語では、関数を変数に代入したり、関数の引数として渡したりすることができます。この際、関数型の変数にnil
が代入されている場合があります。通常、nil
関数値を直接呼び出そうとすると、Goランタイムはパニックを発生させます。これは、プログラマが意図しないnil
参照を早期に検出し、適切なエラーハンドリングを促すためのGoの設計思想に基づいています。
しかし、このコミット以前のGoランタイムでは、go f()
のようにnil
であるf
を新しいゴルーチンとして起動しようとすると、特定の条件下(コミットメッセージによるとm->locks > 0
の場合)で、ランタイムがnil
ポインタをデリファレンス(参照解除)しようとし、その結果として回復不能な致命的エラーが発生していました。致命的エラーは、通常のパニックとは異なり、recover
メカニズムで捕捉することができず、プログラム全体が強制終了してしまうため、デバッグやエラーからの回復が非常に困難でした。
この挙動は、Goの一般的なエラーハンドリングの期待(nil
関数呼び出しはパニックする)と矛盾しており、開発者にとって予期せぬクラッシュを引き起こす可能性がありました。このコミットは、この矛盾を解消し、go nil
関数呼び出しも他のnil
関数呼び出しと同様にパニックを発生させるようにすることで、Goプログラムの堅牢性と予測可能性を向上させることを目的としています。パニックにすることで、開発者はdefer
とrecover
を使ってこの種のランタイムエラーを捕捉し、適切に処理する機会を得ることができます。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語およびGoランタイムに関する基本的な知識が必要です。
-
ゴルーチン (Goroutine): Go言語における軽量な実行スレッドです。
go
キーワードを使って関数呼び出しの前に置くことで、その関数を新しいゴルーチンとして並行して実行させることができます。ゴルーチンはOSのスレッドよりもはるかに軽量であり、数千、数万のゴルーチンを同時に実行することが可能です。 -
go
ステートメント:go
キーワードは、関数呼び出しを新しいゴルーチンで実行するために使用されます。例:go myFunction()
。 -
nil
値と関数型: Goでは、関数も第一級オブジェクトであり、変数に代入したり、引数として渡したり、戻り値として返したりできます。関数型の変数にはnil
を代入することができます。nil
関数値を直接呼び出そうとすると、通常はランタイムパニックが発生します。 -
パニック (Panic) とリカバリー (Recover):
- パニック: Goプログラムが回復不能なエラーに遭遇した際に発生するランタイムエラーです。パニックが発生すると、現在のゴルーチンの通常の実行フローは停止し、遅延関数(
defer
で登録された関数)が実行され、その後、呼び出しスタックを遡ってパニックが伝播します。 - リカバリー:
defer
関数内でrecover()
を呼び出すことで、パニックを捕捉し、パニックが発生したゴルーチンの実行を再開することができます。これにより、プログラム全体がクラッシュするのを防ぎ、エラーを適切に処理する機会が得られます。
- パニック: Goプログラムが回復不能なエラーに遭遇した際に発生するランタイムエラーです。パニックが発生すると、現在のゴルーチンの通常の実行フローは停止し、遅延関数(
-
Goランタイム (Go Runtime): Goプログラムの実行を管理するシステムです。ゴルーチンのスケジューリング、メモリ管理(ガベージコレクション)、チャネル通信、システムコールなど、Goプログラムの低レベルな側面を扱います。ランタイムの多くの部分はC言語(またはGo言語自体)で実装されています。
-
m
(Machine/Thread Context): Goランタイムの内部では、m
はOSのスレッド(マシン)を表す構造体です。各m
は、Goのスケジューラによってゴルーチンを実行するために使用されます。m
構造体には、現在のスレッドの状態やロックに関する情報などが含まれています。 -
m->locks
:m
構造体内のフィールドで、現在のOSスレッドが保持しているランタイムロックの数を表します。この値が0より大きい場合、そのスレッドは重要なランタイム操作を実行中であり、プリエンプション(横取り)が一時的に無効になっていることを示唆します。プリエンプションとは、スケジューラが実行中のゴルーチンを中断し、別のゴルーチンにCPUを割り当てることです。m->locks > 0
の状態でnil
デリファレンスが発生すると、ランタイムの内部状態が不安定になり、回復不能な致命的エラーにつながる可能性がありました。 -
runtime·throw
: Goランタイム内部で使用される関数で、回復不能なランタイムエラーが発生した際にパニックを発生させるために使用されます。これは通常のpanic()
関数とは異なり、より低レベルで、ランタイムの内部状態を考慮してパニックをトリガーします。
技術的詳細
このコミットの技術的な核心は、src/pkg/runtime/proc.c
内のruntime·newproc1
関数における変更です。runtime·newproc1
は、新しいゴルーチンを作成し、その実行を開始するためのランタイム内部関数です。
変更前は、fn
(新しいゴルーチンで実行される関数の値)がnil
である場合、runtime·newproc1
はnil
チェックを行っていませんでした。そのため、後続の処理でfn
がデリファレンスされる際に、nil
ポインタデリファレンスが発生し、特にm->locks > 0
のような特定のランタイム状態下では、回復不能な致命的エラーを引き起こしていました。
このコミットでは、runtime·newproc1
関数の冒頭に以下のnil
チェックが追加されました。
if(fn == nil) {
m->throwing = -1; // do not dump full stacks
runtime·throw("go of nil func value");
}
このコードブロックの動作は以下の通りです。
if(fn == nil)
: 新しいゴルーチンとして起動しようとしている関数値fn
がnil
であるかどうかをチェックします。m->throwing = -1;
:m
は現在のOSスレッドのコンテキストを表す構造体です。m->throwing
は、ランタイムがパニックを処理している状態を示すフラグです。-1
を設定することで、パニック発生時に完全なスタックトレースをダンプしないように指示しています。これは、この特定のケース(go nil
)では、スタックトレースが冗長になる可能性があり、問題の根本原因がnil
関数値のgo
呼び出しであると明確であるため、簡潔なエラーメッセージを優先するための最適化と考えられます。runtime·throw("go of nil func value");
:runtime·throw
関数を呼び出し、指定されたメッセージ「go of nil func value」と共にパニックを発生させます。これにより、プログラムは致命的エラーでクラッシュする代わりに、通常のパニックフローに入り、defer
とrecover
によって捕捉可能になります。
この変更により、go nil
関数呼び出しは、他のnil
関数呼び出しと同様に、一貫してパニックを発生させるようになり、Goプログラムの堅牢性とデバッグのしやすさが向上しました。
また、src/pkg/runtime/crash_test.go
には、この修正を検証するための新しいテストケースTestGoNil
が追加されました。このテストは、goNilSource
という文字列定数で定義されたGoプログラムを実行します。このプログラムは、var f func()
でnil
関数f
を宣言し、go f()
でそれをゴルーチンとして起動しようとします。テストは、この実行が「go of nil func value」というメッセージを含む出力を生成することを確認し、修正が正しく機能していることを保証します。
コアとなるコードの変更箇所
src/pkg/runtime/proc.c
--- a/src/pkg/runtime/proc.c
+++ b/src/pkg/runtime/proc.c
@@ -1816,6 +1816,10 @@ runtime·newproc1(FuncVal *fn, byte *argp, int32 narg, int32 nret, void *callerp
//runtime·printf("newproc1 %p %p narg=%d nret=%d\n", fn->fn, argp, narg, nret);
+ if(fn == nil) {
+ m->throwing = -1; // do not dump full stacks
+ runtime·throw("go of nil func value");
+ }
m->locks++; // disable preemption because it can be holding p in a local var
siz = narg + nret;
siz = (siz+7) & ~7;
src/pkg/runtime/crash_test.go
--- a/src/pkg/runtime/crash_test.go
+++ b/src/pkg/runtime/crash_test.go
@@ -158,6 +158,14 @@ func TestGoexitCrash(t *testing.T) {
}
}
+func TestGoNil(t *testing.T) {
+ output := executeTest(t, goNilSource, nil)
+ want := "go of nil func value"
+ if !strings.Contains(output, want) {
+ t.Fatalf("output:\n%s\n\nwant output containing: %s", output, want)
+ }
+}
+
const crashSource = `
package main
@@ -343,3 +351,16 @@ func main() {
runtime.Goexit()
}
`
+
+const goNilSource = `
+package main
+
+func main() {
+ defer func() {
+ recover()
+ }()
+ var f func()
+ go f()
+ select{}
+}
+`
コアとなるコードの解説
src/pkg/runtime/proc.c
の変更点
runtime·newproc1
関数は、新しいゴルーチンを生成するGoランタイムの内部関数です。この関数は、新しいゴルーチンで実行される関数へのポインタfn
を引数として受け取ります。
追加されたコードブロックは以下の通りです。
if(fn == nil) {
m->throwing = -1; // do not dump full stacks
runtime·throw("go of nil func value");
}
if(fn == nil)
: これは、go
ステートメントに渡された関数値fn
がnil
であるかどうかをチェックする条件分岐です。m->throwing = -1;
:m
は現在のOSスレッド(マシン)のコンテキストを表すグローバル変数です。m->throwing
は、ランタイムがパニック処理中であることを示すフラグです。この値を-1
に設定することで、この特定のパニック(go nil
)が発生した際に、ランタイムが完全なスタックトレースをダンプしないように指示しています。これは、スタックトレースが冗長になるのを避け、問題の根本原因(nil
関数値のgo
呼び出し)を明確に伝えるための最適化です。runtime·throw("go of nil func value");
: この関数呼び出しは、指定された文字列「go of nil func value」をパニックメッセージとして、Goランタイムレベルでパニックを発生させます。これにより、以前は回復不能な致命的エラーでプログラムがクラッシュしていた状況が、recover
で捕捉可能な通常のパニックに変わります。
この変更により、go
ステートメントにnil
関数値が渡された場合でも、ランタイムが安全にパニックを発生させ、プログラムがより予測可能な形で終了するようになります。
src/pkg/runtime/crash_test.go
の変更点
このファイルには、新しいテスト関数TestGoNil
と、そのテストで使用されるソースコードを定義する文字列定数goNilSource
が追加されました。
-
TestGoNil
関数: このテスト関数は、executeTest
ヘルパー関数を使用して、goNilSource
で定義されたGoプログラムを実行します。output := executeTest(t, goNilSource, nil)
その後、実行結果の出力output
が、期待されるパニックメッセージ「go of nil func value」を含んでいるかどうかをstrings.Contains
で確認します。if !strings.Contains(output, want)
これにより、go nil
関数呼び出しが正しくパニックを発生させ、期待されるメッセージを出力することを確認しています。 -
goNilSource
定数: この文字列定数は、TestGoNil
で実行されるGoプログラムのソースコードを定義しています。package main func main() { defer func() { recover() }() var f func() go f() select{} }
このプログラムの重要な点は以下の通りです。
var f func()
: 関数型の変数f
を宣言しますが、初期値はnil
です。go f()
:nil
であるf
を新しいゴルーチンとして起動しようとします。これがこのコミットで修正された問題のトリガーとなります。defer func() { recover() }()
: パニックが発生した場合にそれを捕捉しようとするdefer
関数です。このテストでは、パニックが実際に発生し、そのメッセージが期待通りであることを確認するため、recover
が呼び出されてもテストの成功/失敗には直接影響しませんが、パニックの挙動を検証する上で重要な要素です。select{}
: メインゴルーチンがすぐに終了しないように、ブロッキングするselect
ステートメントです。これにより、go f()
で起動されたゴルーチンが実行される機会が確保されます。
これらの変更により、go nil
関数呼び出しの挙動が修正され、その修正がテストによって検証されるようになりました。
関連リンク
- Go CL (Change List): https://golang.org/cl/97620043
- Go Issue #8045: コミットメッセージに
Fixes #8045
と記載されていますが、公式のGitHubリポジトリでは直接この番号のIssueは見つかりませんでした。これは、内部のバグトラッカーのIDであるか、古いIssueトラッキングシステムのものである可能性があります。しかし、コミットメッセージとコード変更から、問題はgo nil
関数値が致命的エラーを引き起こすことであると明確です。
参考にした情報源リンク
- Go言語公式ドキュメント (Goroutines, Panic and Recover, Functions)
- Goランタイムのソースコード (
src/pkg/runtime/proc.c
のruntime·newproc1
関数周辺) - Goの
m
構造体に関する一般的な情報 (Goランタイムの内部構造に関するブログ記事やドキュメント) - Goの
nil
値に関する一般的な情報