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

[インデックス 11638] ファイルの概要

このコミットは、Go言語のコマンドラインツール cmd/go におけるビルド処理の改善に関するものです。具体的には、go build コマンドが個別のGoファイルを指定して実行された際に、依存パッケージのインポートエラーが発生した場合に、パニック(panic)ではなく、適切なエラーメッセージを出力して終了するように修正しています。

コミット

commit cb0de68a089fd2b05bcf87c4f487b30b96392b5e
Author: Kyle Lemons <kyle@kylelemons.net>
Date:   Mon Feb 6 14:10:03 2012 +1100

                cmd/go: build: print import errors when invoked on files
    
          This fix makes the goFilesPackage helper function print the errors from
          package imports and exit similar to how the packagesForBuild function does.
    
          Without this change, when invoking "go build *.go" with, for example,
          an old import path, the following stack trace is generated:
    
          panic: runtime error: invalid memory address or nil pointer dereference
    
          goroutine 1 [running]:
          go/build.(*Tree).PkgDir(...)
                  /opt/go/src/pkg/go/build/path.go:52 +0xfb
          main.(*builder).action(...)\
                  /opt/go/src/cmd/go/build.go:327 +0xb8
          main.(*builder).action(...)\
                  /opt/go/src/cmd/go/build.go:335 +0x208
          main.runBuild(...)\
                  /opt/go/src/cmd/go/build.go:129 +0x386
          main.main()\
                  /opt/go/src/cmd/go/main.go:126 +0x2d8
    
    Fixes #2865.
    
    R=rsc, dvyukov, r
    CC=golang-dev
    https://golang.org/cl/5624052

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/cb0de68a089fd2b05bcf87c4f487b30b96392b5e

元コミット内容

このコミットの目的は、go build コマンドが個別のGoファイルを指定して実行された際に、依存パッケージのインポートエラーが発生した場合に、パニックではなく、適切なエラーメッセージを出力して終了するようにすることです。

具体的には、goFilesPackage ヘルパー関数が、packagesForBuild 関数と同様に、パッケージインポートからのエラーを出力し、終了するように修正されています。

この変更がなければ、例えば古いインポートパスを持つファイルに対して "go build *.go" のようにコマンドを実行すると、以下のようなスタックトレースを伴うパニックが発生していました。

panic: runtime error: invalid memory address or nil pointer dereference

goroutine 1 [running]:
go/build.(*Tree).PkgDir(...)
        /opt/go/src/pkg/go/build/path.go:52 +0xfb
main.(*builder).action(...)
        /opt/go/src/cmd/go/build.go:327 +0xb8
main.(*builder).action(...)
        /opt/go/src/cmd/go/build.go:335 +0x208
main.runBuild(...)
        /opt/go/src/cmd/go/build.go:129 +0x386
main.main()
        /opt/go/src/cmd/go/main.go:126 +0x2d8

このコミットは、Issue #2865 を修正します。

変更の背景

Go言語のビルドツール go build は、開発者がGoプログラムをコンパイルする際に日常的に使用する重要なコマンドです。通常、go build はパッケージ単位で動作しますが、特定のGoファイルを直接指定してビルドすることも可能です(例: go build main.gogo build *.go)。

このコミットが作成された当時の go build コマンドには、特定のシナリオで問題がありました。それは、ユーザーが個別のGoファイルを指定してビルドしようとした際に、そのファイルが依存しているパッケージのインポートパスに問題がある場合(例えば、存在しないパッケージをインポートしようとしている、あるいは古いインポートパスを使用しているなど)に、ツールが予期せぬパニックを起こしてしまうというものでした。

パニックは、プログラムが回復不能なエラーに遭遇した際に発生するGoのメカニズムですが、ユーザーにとっては非常に不親切な挙動です。特に、インポートエラーのような、よりユーザーフレンドリーなエラーメッセージで対処できるはずの問題でパニックが発生すると、デバッグが困難になり、開発体験を著しく損ないます。

この問題は、GoのIssueトラッカーで #2865 として報告されていました。ユーザーは、インポートエラーが原因で go build がクラッシュするのではなく、明確なエラーメッセージを出力して終了することを期待していました。このコミットは、その期待に応え、ツールの堅牢性とユーザーフレンドリーさを向上させるために導入されました。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびGoツールの基本的な概念を理解しておく必要があります。

  1. Go言語のパッケージシステム:

    • Goのコードは「パッケージ」という単位で整理されます。関連する機能は同じパッケージにまとめられ、他のパッケージからインポートして利用されます。
    • import ステートメントは、他のパッケージの機能を利用するために使用されます。インポートパスは、通常、Goモジュールのパスや標準ライブラリのパスに対応します。
    • go build コマンドは、これらのパッケージ間の依存関係を解決し、実行可能なバイナリやライブラリを生成します。
  2. go build コマンド:

    • Goソースコードをコンパイルするための主要なコマンドです。
    • 引数なしで実行すると、現在のディレクトリのパッケージをビルドします。
    • パッケージパス(例: go build github.com/user/repo/mypkg)を指定して特定のパッケージをビルドできます。
    • Goファイル名(例: go build main.gogo build *.go)を指定して、個別のファイルをビルドすることもできます。この場合、指定されたファイル群が単一のパッケージとして扱われます。
  3. Goのビルドプロセスとエラーハンドリング:

    • go build の内部では、ソースファイルの解析、依存関係の解決、コンパイル、リンクといった一連のステップが実行されます。
    • このプロセス中に、構文エラー、型エラー、未解決のインポートパスなど、様々な種類のエラーが発生する可能性があります。
    • Goツールは通常、これらのエラーを検出し、ユーザーに分かりやすいメッセージを出力して終了します。
    • パニック (panic): Goにおけるパニックは、プログラムが回復不能な状態に陥ったことを示すメカニズムです。通常、プログラミングエラー(例: nilポインタ参照、配列の範囲外アクセス)によって引き起こされます。パニックが発生すると、現在のゴルーチンは実行を停止し、遅延関数が実行された後、スタックトレースが出力されてプログラムが終了します。開発ツールにおいては、ユーザー入力や環境に起因するエラーでパニックが発生するのは望ましくありません。
  4. cmd/go の内部構造:

    • cmd/go は、go コマンドの実装を含むGoの標準ライブラリの一部です。
    • このツールは、ビルド、テスト、フォーマットなど、様々なサブコマンドを処理するための複雑なロジックを含んでいます。
    • goFilesPackagepackagesForBuild といった関数は、Goソースファイルの解析、パッケージ情報の収集、ビルド対象の決定など、ビルドプロセスの異なる段階を処理する内部ヘルパー関数です。
    • Package 構造体は、Goパッケージに関するメタデータ(名前、インポートパス、依存関係、エラーなど)を保持します。特に pkg.DepsErrors は、依存パッケージの解決中に発生したエラーのリストを保持するために使用されます。
    • fatalf および errorf は、cmd/go ツール内でエラーメッセージを出力し、必要に応じてプログラムを終了させるためのユーティリティ関数です。exitIfErrors() は、これまでに記録されたエラーがあれば、それらを出力してプログラムを終了させる関数です。

このコミットは、go build が個別のファイルを扱う際の goFilesPackage 関数が、依存関係の解決中に発生したエラーを適切に処理せず、結果としてパニックを引き起こしていた問題を修正しています。

技術的詳細

このコミットの技術的な核心は、cmd/go ツールがGoソースファイルを解析し、パッケージの依存関係を解決する際の、エラーハンドリングの改善にあります。

Goのビルドプロセスでは、ソースコードを解析して、そのファイルがどのパッケージに属し、どの外部パッケージに依存しているかを特定します。この依存関係の解決中に、インポートパスが間違っていたり、参照しているパッケージが見つからなかったりすると、エラーが発生します。

変更前の goFilesPackage 関数は、自身のパッケージ(pkg.Error)に関する直接的なエラーは fatalf を使って処理していましたが、依存パッケージ(pkg.DepsErrors)に関するエラーは適切に処理していませんでした。その結果、pkg.DepsErrors にエラーが含まれているにもかかわらず、後続の処理が nil ポインタを参照しようとしてパニックを引き起こしていました。コミットメッセージに示されているスタックトレースは、go/build.(*Tree).PkgDir(...)nil ポインタ参照が発生していることを示しており、これは依存パッケージのディレクトリ解決に失敗した結果、PkgDir が予期せぬ nil 値を受け取ったためと考えられます。

このコミットでは、goFilesPackage 関数に以下のロジックが追加されました。

  1. pkg.DepsErrors のチェック: pkg.DepsErrors スライスをイテレートし、依存パッケージのインポートエラーが存在するかどうかを確認します。
  2. 重複エラーの抑制: printed := map[error]bool{} というマップを使用して、同じエラーメッセージが複数回出力されるのを防ぎます。これは、複数のパッケージが同じ依存関係の問題を抱えている場合に、エラーメッセージが冗長になるのを避けるためです。
  3. エラーメッセージの出力: 各ユニークな依存エラーに対して errorf("%s", err) を呼び出し、エラーメッセージを標準エラー出力に出力します。errorf は、エラーを記録し、必要に応じてプログラムの終了を準備する cmd/go の内部関数です。
  4. exitIfErrors() の呼び出し: 依存エラーの処理ループの後、exitIfErrors() が呼び出されます。この関数は、これまでに errorffatalf によって記録されたエラーが存在する場合、それらを出力し、プログラムを非ゼロの終了コードで終了させます。これにより、パニックを回避し、ユーザーに明確なエラー情報を提供して、ツールが正常に終了するようになります。

この修正により、go build *.go のようなコマンドでインポートエラーが発生した場合でも、ツールはパニックすることなく、問題のあるインポートパスをユーザーに通知し、適切な終了コードで終了するようになります。これは、ツールの堅牢性を高め、開発者のデバッグ体験を向上させる上で非常に重要な改善です。

コアとなるコードの変更箇所

変更は src/cmd/go/build.go ファイルに集中しています。

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -304,6 +304,17 @@ func goFilesPackage(gofiles []string, target string) *Package {
 	if pkg.Error != nil {
 		fatalf("%s", pkg.Error)
 	}
+	printed := map[error]bool{}
+	for _, err := range pkg.DepsErrors {
+		// Since these are errors in dependencies,
+		// the same error might show up multiple times,
+		// once in each package that depends on it.
+		// Only print each once.
+		if !printed[err] {
+			printed[err] = true
+			errorf("%s", err)
+		}
+	}
 	if target != "" {
 		pkg.target = target
 	} else if pkg.Name == "main" {
@@ -312,6 +323,7 @@ func goFilesPackage(gofiles []string, target string) *Package {\
 		pkg.target = pkg.Name + ".a"
 	}
 	pkg.ImportPath = "_/" + pkg.target
+	exitIfErrors()
 	return pkg
 }

コアとなるコードの解説

追加されたコードブロックは goFilesPackage 関数の内部にあります。

  1. printed := map[error]bool{}:

    • これは、map[error]bool 型のマップ printed を初期化しています。このマップは、既に出力されたエラーを追跡するために使用されます。Goのエラーはインターフェース型であるため、マップのキーとして使用できます。
    • 目的は、複数の依存関係が同じ根本的なエラー(例: 存在しない共通のライブラリ)を報告する場合に、同じエラーメッセージが繰り返し表示されるのを防ぐことです。
  2. for _, err := range pkg.DepsErrors { ... }:

    • pkg.DepsErrors は、現在のパッケージが依存している他のパッケージの解決中に発生したエラーのリストを保持するスライスです。
    • この for ループは、pkg.DepsErrors 内の各エラーを順番に処理します。
  3. if !printed[err] { ... }:

    • この条件文は、現在処理している errprinted マップにまだ存在しない(つまり、まだ出力されていない)場合にのみ、内部のブロックを実行します。
    • これにより、エラーの重複出力が防止されます。
  4. printed[err] = true:

    • エラーが初めて検出された場合、そのエラーを printed マップに追加し、値 true を設定します。これにより、次回同じエラーが検出されても、この条件文が false となり、再度出力されることはありません。
  5. errorf("%s", err):

    • これは cmd/go ツール内で定義されているヘルパー関数で、フォーマットされたエラーメッセージを標準エラー出力に書き込みます。
    • 重要なのは、errorf がエラーを記録するだけでなく、プログラムの終了ステータスに影響を与える可能性があることです。errorf が呼び出されると、ツールはエラー状態になり、最終的に exitIfErrors() が呼び出された際に非ゼロの終了コードで終了します。
  6. exitIfErrors():

    • この行は、pkg.ImportPath が設定された直後に追加されています。
    • exitIfErrors() は、これまでに errorffatalf によって記録されたエラーが存在するかどうかをチェックし、もし存在すれば、それらのエラーメッセージを出力してプログラムを終了させます。
    • この呼び出しにより、goFilesPackage が依存エラーを検出した場合に、後続の処理でパニックが発生する前に、ツールが適切に終了するようになります。

この変更により、go build が個別のファイルを扱う際の堅牢性が大幅に向上し、開発者にとってより予測可能で使いやすいツールとなりました。

関連リンク

参考にした情報源リンク