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

[インデックス 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つの主要なメカニズムがあります。

  1. エラー (Error): 予期されるが異常な状態(例: ファイルが見つからない、ネットワーク接続がタイムアウトする)を扱うためのものです。Goでは、関数がエラーを返すことで明示的にエラーを伝播させ、呼び出し元がif err != nilのような形でエラーをチェックし、適切に処理することが推奨されます。

  2. パニック (Panic): 予期されない、回復不可能なプログラミングエラーや異常な状態(例: nilポインタのデリファレンス、配列の範囲外アクセス)を扱うためのものです。パニックが発生すると、現在の関数の実行は即座に停止し、その関数に遅延された(deferされた)関数が実行されます。その後、呼び出し元の関数へとパニックが伝播し、最終的にメインゴルーチンに到達するとプログラムは終了し、スタックトレースが出力されます。

defer

defer文は、そのdefer文を含む関数が終了する直前(return文の実行後、またはパニック発生時)に、指定された関数呼び出しを実行することを保証します。これは、リソースの解放(ファイルのクローズ、ロックの解除など)や、パニックからの回復処理によく使用されます。

recover関数

recover関数は、defer関数内でのみ意味を持ちます。recoverdefer関数内で呼び出された場合、もし現在のゴルーチンがパニック状態であれば、recoverはそのパニック値を捕捉し、パニックの伝播を停止させます。recoverはパニック値を返しますが、パニック状態でない場合はnilを返します。

recoverを使用することで、パニックが発生してもプログラムがクラッシュするのを防ぎ、エラーをログに記録したり、代替処理を実行したりすることができます。しかし、このコミットの背景で述べられているように、recoverがパニックを捕捉してしまうと、パニック発生時の完全なスタックトレースが失われることがあります。これは、recoverがパニックの伝播を停止させるため、パニックがどこで発生したかを示す詳細な情報がログに出力されなくなるためです。

スタックトレース

スタックトレースは、プログラムが特定の時点(特にエラーやパニックが発生した時点)で実行していた関数の呼び出し履歴を示すリストです。これにより、どの関数がどの関数を呼び出し、最終的にエラーが発生した場所に至ったのかを追跡することができます。デバッグにおいて非常に重要な情報源となります。

技術的詳細

このコミットは、misc/dashboard/builder/main.goファイル内の複数の箇所からdeferrecoverの組み合わせを削除しています。具体的には、以下の関数からrecoverブロックが削除されました。

  1. func (b *Builder) build() bool
  2. func (b *Builder) buildHash(hash string) error (変更前は (err error) を返していた)
  3. func commitPoll(key, pkg string)
  4. 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言語のdeferrecoverの組み合わせを削除したことです。

変更前:

各関数には、以下のようなパターンでdeferrecoverが設定されていました。

defer func() {
    err := recover()
    if err != nil {
        // パニックを捕捉し、ログに記録
        log.Println("panic occurred:", err)
        // 必要に応じて、err変数を設定してエラーとして返す
    }
}()

このコードは、関数内でパニックが発生した場合にそれを捕捉し、プログラムがクラッシュするのを防ぎ、代わりにエラーメッセージをログに出力することを目的としていました。しかし、recoverがパニックを捕捉すると、Goランタイムが通常出力する詳細なスタックトレースが失われてしまいます。これは、パニックが伝播を停止するため、パニックの発生源を特定するための情報が不足するためです。

変更後:

deferrecoverのブロックが完全に削除されました。

// defer func() { ... }() // 削除

この変更により、これらの関数内でパニックが発生した場合、パニックは捕捉されずにそのまま伝播します。パニックがメインゴルーチンに到達すると、Goランタイムはプログラムを終了させ、その際にパニック発生時の完全なスタックトレースを標準エラー出力に書き出します。これにより、開発者は問題の根本原因をより正確に特定できるようになります。

また、buildHashfullHash関数の戻り値の型が変更され、errという名前付き戻り値が削除されています。これは、以前のdeferブロック内でerrにパニック情報を設定していたロジックが不要になったため、より簡潔な関数シグネチャになったことを示しています。エラーハンドリングは、明示的にreturn errとする形に統一されています。

この変更は、Go言語におけるエラーとパニックの扱いに関するベストプラクティスを反映しています。すなわち、回復可能なエラーは明示的にerror型で返し、回復不可能なプログラミングエラーはパニックとして扱い、完全なスタックトレースを伴ってプログラムを終了させることで、早期に問題を特定し修正するというアプローチです。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のブログ
  • GitHubのコミット履歴
  • Go言語のソースコード
  • Go言語のエラーハンドリングに関する一般的な知識