[インデックス 11460] ファイルの概要
このコミットは、Go言語のダッシュボードビルダにおけるrecover
ブロックの削除に関するものです。recover
はGo言語のパニック(ランタイムエラー)からの回復メカニズムですが、この変更では、予期せぬパニックが発生した場合にrecover
が有用なスタックトレースを隠蔽してしまうという問題に対処するため、明示的にrecover
を使用していた箇所を削除し、プログラムがクラッシュする(パニックが伝播する)ように修正しています。これにより、問題発生時のデバッグ情報がより明確になることを目的としています。
コミット
commit 0f2659a3235d388153e8d0d259800318f5fa7476
Author: Andrew Gerrand <adg@golang.org>
Date: Mon Jan 30 14:53:48 2012 +1100
builder: drop recover blocks
The one time they recovered from anything they obscured a useful stack
trace. We're better off just crashing hard.
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5577073
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0f2659a3235d388153e8d0d259800318f5fa7476
元コミット内容
builder: drop recover blocks
The one time they recovered from anything they obscured a useful stack trace. We're better off just crashing hard.
このコミットメッセージは、ビルダ(おそらくGo言語のCI/CDシステムの一部であるダッシュボードビルダ)において、recover
ブロックを削除したことを示しています。その理由として、recover
が一度でも何かを回復した際に、有用なスタックトレースを隠蔽してしまった経験があり、それよりもプログラムが「ハードにクラッシュする」(つまり、パニックが捕捉されずにプログラムが終了する)方が良いという判断が下されたことを述べています。
変更の背景
Go言語では、予期せぬエラーや異常な状態が発生した場合に「パニック(panic)」というメカニズムが使用されます。パニックが発生すると、通常のプログラムフローは中断され、遅延関数(defer
)が実行された後、プログラムは終了します。しかし、recover
関数をdefer
関数内で呼び出すことで、パニックを捕捉し、プログラムの実行を継続させることが可能です。
このコミットの背景には、Go言語のビルドシステムやダッシュボードが、ビルドプロセス中の予期せぬエラー(パニック)を捕捉するためにrecover
を使用していたという経緯があります。しかし、開発チームは、recover
がパニックを捕捉してしまった結果、本来デバッグに不可欠な詳細なスタックトレースが失われ、問題の原因特定が困難になるという問題に直面しました。
特に、ビルドシステムのような重要なインフラストラクチャにおいては、エラー発生時に可能な限り多くの診断情報を得ることが極めて重要です。スタックトレースが不完全であると、開発者はエラーの根本原因を特定するために多くの時間を費やすことになります。そのため、recover
による「回復」よりも、「完全なスタックトレースを伴うクラッシュ」の方が、長期的なデバッグとシステムの安定性にとって有益であると判断されました。
この変更は、Go言語の設計哲学の一部である「エラーは明示的に処理されるべきであり、予期せぬパニックは早期に、そして明確に報告されるべきである」という考え方にも合致しています。
前提知識の解説
Go言語のパニックとリカバリー (Panic and Recover)
Go言語には、エラーハンドリングのための2つの主要なメカニズムがあります。
-
エラー (Error): 予期されるが異常な状態(例: ファイルが見つからない、ネットワーク接続がタイムアウトする)を扱うためのものです。Goでは、関数がエラーを返すことで明示的にエラーを伝播させ、呼び出し元が
if err != nil
のような形でエラーをチェックし、適切に処理することが推奨されます。 -
パニック (Panic): 予期されない、回復不可能なプログラミングエラーや異常な状態(例: nilポインタのデリファレンス、配列の範囲外アクセス)を扱うためのものです。パニックが発生すると、現在の関数の実行は即座に停止し、その関数に遅延された(
defer
された)関数が実行されます。その後、呼び出し元の関数へとパニックが伝播し、最終的にメインゴルーチンに到達するとプログラムは終了し、スタックトレースが出力されます。
defer
文
defer
文は、そのdefer
文を含む関数が終了する直前(return
文の実行後、またはパニック発生時)に、指定された関数呼び出しを実行することを保証します。これは、リソースの解放(ファイルのクローズ、ロックの解除など)や、パニックからの回復処理によく使用されます。
recover
関数
recover
関数は、defer
関数内でのみ意味を持ちます。recover
がdefer
関数内で呼び出された場合、もし現在のゴルーチンがパニック状態であれば、recover
はそのパニック値を捕捉し、パニックの伝播を停止させます。recover
はパニック値を返しますが、パニック状態でない場合はnil
を返します。
recover
を使用することで、パニックが発生してもプログラムがクラッシュするのを防ぎ、エラーをログに記録したり、代替処理を実行したりすることができます。しかし、このコミットの背景で述べられているように、recover
がパニックを捕捉してしまうと、パニック発生時の完全なスタックトレースが失われることがあります。これは、recover
がパニックの伝播を停止させるため、パニックがどこで発生したかを示す詳細な情報がログに出力されなくなるためです。
スタックトレース
スタックトレースは、プログラムが特定の時点(特にエラーやパニックが発生した時点)で実行していた関数の呼び出し履歴を示すリストです。これにより、どの関数がどの関数を呼び出し、最終的にエラーが発生した場所に至ったのかを追跡することができます。デバッグにおいて非常に重要な情報源となります。
技術的詳細
このコミットは、misc/dashboard/builder/main.go
ファイル内の複数の箇所からdefer
とrecover
の組み合わせを削除しています。具体的には、以下の関数からrecover
ブロックが削除されました。
func (b *Builder) build() bool
func (b *Builder) buildHash(hash string) error
(変更前は(err error)
を返していた)func commitPoll(key, pkg string)
func fullHash(root, rev string) (string, error)
(変更前は(hash string, err error)
を返していた)
これらの関数は、Go言語のビルドシステムの一部であり、ビルドの実行、コミットのポーリング、ハッシュの取得といった重要な操作を担当しています。以前は、これらの操作中にパニックが発生した場合に備えてrecover
が設定されており、パニックを捕捉してログに記録する試みが行われていました。
しかし、コミットメッセージが示唆するように、このrecover
の利用は、デバッグ時に必要な完全なスタックトレースを隠蔽してしまうという副作用をもたらしていました。例えば、build()
関数内のrecover
ブロックは、パニックが発生した場合にlog.Println(b.name, "build:", err)
としてエラーを記録するだけで、パニックの発生源を示す詳細なスタックトレースは出力されませんでした。
この変更により、これらの関数内でパニックが発生した場合、recover
によって捕捉されることなく、パニックは呼び出しスタックを遡って伝播し、最終的にプログラム全体を終了させます。この際、Goランタイムはパニック発生時の完全なスタックトレースを標準エラー出力に書き出すため、開発者はより詳細なデバッグ情報を得られるようになります。
また、buildHash
関数とfullHash
関数のシグネチャも変更されています。
func (b *Builder) buildHash(hash string) (err error)
からfunc (b *Builder) buildHash(hash string) error
へfunc fullHash(root, rev string) (hash string, err error)
からfunc fullHash(root, rev string) (string, error)
へ
これは、以前のdefer
ブロック内でerr
変数を操作していた名残を削除し、よりGoらしいエラーハンドリング(エラーを直接返す)に統一するためと考えられます。recover
を削除したことで、パニックはエラーとして扱われるのではなく、プログラムの異常終了として扱われるため、関数の戻り値としてerr
を操作する必要がなくなりました。
コアとなるコードの変更箇所
変更はmisc/dashboard/builder/main.go
ファイルに集中しています。
func (b *Builder) build() bool
--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -230,12 +230,6 @@ func (b *Builder) buildExternal() {
// and builds it if one is found.
// It returns true if a build was attempted.
func (b *Builder) build() bool {
-- defer func() {
-- err := recover()
-- if err != nil {
-- log.Println(b.name, "build:", err)
-- }
-- }()
hash, err := b.todo("build-go-commit", "", "")
if err != nil {
log.Println(err)
func (b *Builder) buildHash(hash string) (err error)
--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -245,7 +239,6 @@ func (b *Builder) build() bool {
return false
}
// Look for hash locally before running hg pull.
--
if _, err := fullHash(goroot, hash[:12]); err != nil {
// Don't have hash, so run hg pull.
if err := run(nil, goroot, "hg", "pull"); err != nil {
@@ -260,33 +253,24 @@ func (b *Builder) build() bool {
return true
}
-func (b *Builder) buildHash(hash string) (err error) {
-- defer func() {
-- if err != nil {
-- err = fmt.Errorf("%s build: %s: %s", b.name, hash, err)
-- }
-- }()
--
-+func (b *Builder) buildHash(hash string) error {
log.Println(b.name, "building", hash)
// create place in which to do work
workpath := path.Join(*buildroot, b.name+"-"+hash[:12])
-- err = os.Mkdir(workpath, mkdirPerm)
-- if err != nil {\n-\t\treturn\n+\tif err := os.Mkdir(workpath, mkdirPerm); err != nil {
+\t\treturn err
}
defer os.RemoveAll(workpath)
// clone repo
-- err = run(nil, workpath, "hg", "clone", goroot, "go")
-- if err != nil {\n-\t\treturn\n+\tif err := run(nil, workpath, "hg", "clone", goroot, "go"); err != nil {
+\t\treturn err
}
// update to specified revision
-- err = run(nil, path.Join(workpath, "go"), "hg", "update", hash)
-- if err != nil {\n-\t\treturn\n+\tif err := run(nil, path.Join(workpath, "go"), "hg", "update", hash); err != nil {
+\t\treturn err
}
srcDir := path.Join(workpath, "go", "src")
@@ -323,24 +307,22 @@ func (b *Builder) buildHash(hash string) (err error) {
// finish here if codeUsername and codePassword aren't set
if b.codeUsername == "" || b.codePassword == "" || !*buildRelease {
-- return
-+ return nil
}
// if this is a release, create tgz and upload to google code
releaseHash, release, err := firstTag(binaryTagRe)
if hash == releaseHash {
// clean out build state
-- err = run(b.envv(), srcDir, "./clean.bash", "--nopkg")
-- if err != nil {\n-\t\t\treturn fmt.Errorf("clean.bash: %s", err)
+\t\tif err := run(b.envv(), srcDir, "./clean.bash", "--nopkg"); err != nil {
+\t\t\treturn fmt.Errorf("clean.bash: %s", err)
}
// upload binary release
fn := fmt.Sprintf("go.%s.%s-%s.tar.gz", release, b.goos, b.goarch)
-- err = run(nil, workpath, "tar", "czf", fn, "go")
-- if err != nil {\n-\t\t\treturn fmt.Errorf("tar: %s", err)
+\t\tif err := run(nil, workpath, "tar", "czf", fn, "go"); err != nil {
+\t\t\treturn fmt.Errorf("tar: %s", err)
}
-- err = run(nil, workpath, path.Join(goroot, codePyScript),
+\t\terr := run(nil, workpath, path.Join(goroot, codePyScript),
"-s", release,
"-p", codeProject,
"-u", b.codeUsername,
@@ -352,7 +334,7 @@ func (b *Builder) buildHash(hash string) (err error) {
}
}
-- return
-+ return nil
}
func (b *Builder) buildSubrepos(goRoot, goHash string) {
func commitPoll(key, pkg string)
--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -571,13 +553,6 @@ const xmlLogTemplate = `
// commitPoll pulls any new revisions from the hg server
// and tells the server about them.
func commitPoll(key, pkg string) {
-- // Catch unexpected panics.
-- defer func() {
-- if err := recover(); err != nil {
-- log.Printf("commitPoll panic: %s", err)
-- }
-- }()
--
pkgRoot := goroot
if pkg != "" {
func fullHash(root, rev string) (hash string, err error)
--- a/misc/dashboard/builder/main.go
+++ b/misc/dashboard/builder/main.go
@@ -687,12 +662,7 @@ func addCommit(pkg, hash, key string) bool {
}
// fullHash returns the full hash for the given Mercurial revision.
-func fullHash(root, rev string) (hash string, err error) {
-- defer func() {
-- if err != nil {
-- err = fmt.Errorf("fullHash: %s: %s", rev, err)
-- }
-- }()
-+func fullHash(root, rev string) (string, error) {
s, _, err := runLog(nil, "", root,
"hg", "log",
"--encoding=utf-8",
@@ -701,7 +671,7 @@ func fullHash(root, rev string) (hash string, err error) {
"--template={node}",
)
if err != nil {
-- return
-+ return "", nil
}
s = strings.TrimSpace(s)
if s == "" {
コアとなるコードの解説
このコミットの主要な変更点は、Go言語のdefer
とrecover
の組み合わせを削除したことです。
変更前:
各関数には、以下のようなパターンでdefer
とrecover
が設定されていました。
defer func() {
err := recover()
if err != nil {
// パニックを捕捉し、ログに記録
log.Println("panic occurred:", err)
// 必要に応じて、err変数を設定してエラーとして返す
}
}()
このコードは、関数内でパニックが発生した場合にそれを捕捉し、プログラムがクラッシュするのを防ぎ、代わりにエラーメッセージをログに出力することを目的としていました。しかし、recover
がパニックを捕捉すると、Goランタイムが通常出力する詳細なスタックトレースが失われてしまいます。これは、パニックが伝播を停止するため、パニックの発生源を特定するための情報が不足するためです。
変更後:
defer
とrecover
のブロックが完全に削除されました。
// defer func() { ... }() // 削除
この変更により、これらの関数内でパニックが発生した場合、パニックは捕捉されずにそのまま伝播します。パニックがメインゴルーチンに到達すると、Goランタイムはプログラムを終了させ、その際にパニック発生時の完全なスタックトレースを標準エラー出力に書き出します。これにより、開発者は問題の根本原因をより正確に特定できるようになります。
また、buildHash
とfullHash
関数の戻り値の型が変更され、err
という名前付き戻り値が削除されています。これは、以前のdefer
ブロック内でerr
にパニック情報を設定していたロジックが不要になったため、より簡潔な関数シグネチャになったことを示しています。エラーハンドリングは、明示的にreturn err
とする形に統一されています。
この変更は、Go言語におけるエラーとパニックの扱いに関するベストプラクティスを反映しています。すなわち、回復可能なエラーは明示的にerror
型で返し、回復不可能なプログラミングエラーはパニックとして扱い、完全なスタックトレースを伴ってプログラムを終了させることで、早期に問題を特定し修正するというアプローチです。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/
- Go言語のエラーハンドリングに関する公式ブログ記事 (A Tour of Go - Errors): https://go.dev/tour/basics/16
- Go言語のパニックとリカバリーに関する公式ブログ記事 (Defer, Panic, and Recover): https://go.dev/blog/defer-panic-and-recover
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のブログ
- GitHubのコミット履歴
- Go言語のソースコード
- Go言語のエラーハンドリングに関する一般的な知識