[インデックス 16648] ファイルの概要
このコミットは、Goコマンドラインツール(cmd/go
)において、ディレクトリやパッケージをスキャンする際に発生するコンパイルエラーが、以前はサイレントに無視されていた問題を修正するものです。具体的には、インポート文が解析不能であるなど、パッケージがインポートできない場合に、そのエラーがユーザーに表示されず、パッケージが存在しないかのように扱われていた挙動を改善し、エラーメッセージを適切に出力するように変更されました。
コミット
- コミットハッシュ:
53a00e2812891b2a3c91aaa6a12ac89c74ad42ea
- Author: Rob Pike r@golang.org
- Date: Wed Jun 26 10:48:04 2013 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/53a00e2812891b2a3c91aaa6a12ac89c74ad42ea
元コミット内容
cmd/go: log compilation errors when scanning directories and packages
Before, some packages disappear silently if the package cannot be imported,
such as if the import statement is unparseable.
Before:
% ls src
foo issue
% go list ./...
_/home/r/bug/src/foo
%
After:
% go list ./...\n
src/issue/issue.go:3:5: expected 'STRING', found newline
_/home/r/bug/src/foo
%
R=rsc
CC=golang-dev
https://golang.org/cl/10568043
変更の背景
Goのビルドシステムでは、go list
コマンドなどを用いてパッケージをスキャンする際、特定の条件下でエラーが発生しても、そのエラーがユーザーに通知されずにパッケージがリストから「サイレントに消える」という問題がありました。
具体的には、以下のようなシナリオが考えられます。
- 構文エラーのあるGoソースファイル: 例えば、
import
文に構文エラーがある場合など、Goのパーサーがファイルを正しく解析できないことがあります。 build.ImportDir
の挙動:build
パッケージのImportDir
関数は、指定されたディレクトリからGoパッケージをインポートしようとします。この際、構文エラーなどによってインポートに失敗した場合、エラーを返しますが、そのエラーが上位のcmd/go
コマンドで適切に処理されていませんでした。- サイレントな無視: 以前の実装では、
ImportDir
がエラーを返した場合、そのエラーが単に無視され、該当するパッケージがgo list
の出力に含まれないという挙動になっていました。これにより、ユーザーはなぜ特定のパッケージがリストされないのか理解できず、デバッグが困難になるという問題がありました。
このコミットは、このようなサイレントなエラーを捕捉し、ユーザーに明示的に通知することで、開発体験を向上させることを目的としています。
前提知識の解説
go list
コマンド
go list
コマンドは、Goパッケージに関する情報を表示するために使用されます。例えば、go list ./...
は現在のディレクトリ以下のすべてのパッケージを再帰的にリストアップします。このコマンドは、Goのビルドシステムがどのようにパッケージを認識しているかを確認する上で非常に重要です。
build
パッケージ
Goの標準ライブラリに含まれるgo/build
パッケージは、Goのソースコードを解析し、パッケージの依存関係を解決するための機能を提供します。
build.ImportDir(path string, mode build.ImportMode)
: この関数は、指定されたpath
にあるディレクトリからGoパッケージをインポートしようとします。成功すればパッケージ情報(*build.Package
)を返しますが、失敗した場合はエラーを返します。このエラーには、構文エラーやファイルが見つからないなどの様々な原因が含まれます。build.NoGoError
:build
パッケージが返すエラー型の一つで、指定されたディレクトリにGoのソースファイルが見つからなかった場合に発生します。これは、ディレクトリが空であるか、Goのソースファイルではないファイルのみが含まれている場合に返されます。このエラーは、コンパイルエラーとは異なり、Goのソースファイルが存在しないことを示すため、通常は無視しても問題ないケースが多いです。
log.Print
log
パッケージは、Goプログラムでログメッセージを出力するための標準パッケージです。log.Print(v ...interface{})
は、引数をデフォルトのフォーマットで標準エラー出力に書き込みます。このコミットでは、build.ImportDir
が返したエラーメッセージをユーザーに表示するために使用されています。
型アサーション err.(*build.NoGoError)
Goでは、インターフェース型(ここではerror
インターフェース)の変数が、特定の具象型(ここでは*build.NoGoError
)の値を持っているかどうかを確認し、もし持っていればその具象型の値として取り出すために型アサーションを使用します。
if _, noGo := err.(*build.NoGoError); !noGo { ... }
という構文は、以下の意味を持ちます。
err.(*build.NoGoError)
:err
が*build.NoGoError
型であるかをチェックします。noGo
: 型アサーションが成功した場合(err
が*build.NoGoError
型であった場合)、noGo
はtrue
になり、err
の具象値が*build.NoGoError
型として取り出されます(ここでは_
で破棄されています)。失敗した場合はnoGo
はfalse
になります。!noGo
:err
が*build.NoGoError
型でなかった場合(つまり、Goソースファイルが見つからない以外のエラーであった場合)に、続くブロックの処理を実行します。
このパターンは、特定のエラー型を区別して処理するためにGoでよく用いられるイディオムです。
技術的詳細
このコミットの核心は、src/cmd/go/main.go
内のmatchPackages
およびmatchPackagesInFS
関数におけるエラーハンドリングの改善です。これらの関数は、ファイルシステムをスキャンしてGoパッケージを特定する役割を担っています。
以前の実装では、buildContext.ImportDir
(またはbuild.ImportDir
)がエラーを返した場合、そのエラーがbuild.NoGoError
であるかどうかを文字列比較(strings.Contains(err.Error(), "no Go source files")
)で判断していました。build.NoGoError
であれば、それはGoソースファイルが見つからないことを意味するため、パッケージとして扱わず、エラーメッセージも出力せずにreturn nil
していました。しかし、build.NoGoError
以外のエラー(例えば、構文エラーによるインポート失敗)の場合も、単にreturn nil
してしまい、エラーメッセージがユーザーに表示されないという問題がありました。
新しい実装では、このエラーハンドリングがより堅牢になりました。
- 型アサーションによるエラーの識別:
if _, noGo := err.(*build.NoGoError); !noGo { ... }
という型アサーションを使用することで、エラーがbuild.NoGoError
型であるかどうかを正確に判断するようになりました。文字列比較に比べて、より安全で意図が明確な方法です。 - 非
NoGoError
のエラーのログ出力:!noGo
、つまりエラーがbuild.NoGoError
ではない場合(Goソースファイルが見つからない以外のエラー、例えば構文エラーなど)に、log.Print(err)
を呼び出してエラーメッセージを標準エラー出力に表示するように変更されました。これにより、ユーザーはパッケージのインポートに失敗した具体的な理由を知ることができるようになりました。 NoGoError
の継続的な無視:build.NoGoError
の場合には、引き続きreturn nil
することで、Goソースファイルが存在しないディレクトリはパッケージとして扱わないという従来の挙動を維持しています。これは、意図的にGoソースファイルを含まないディレクトリ(例えば、ドキュメントのみのディレクトリなど)をgo list
の対象から除外するために必要な挙動です。
この変更により、go list
などのコマンドが、Goソースファイルに起因するコンパイルエラーをより透過的に報告するようになり、開発者が問題を迅速に特定し、修正できるようになりました。
コアとなるコードの変更箇所
変更はsrc/cmd/go/main.go
ファイルに集中しています。
--- a/src/cmd/go/main.go
+++ b/src/cmd/go/main.go
@@ -486,6 +486,9 @@ func matchPackages(pattern string) []string {\n \t\t}\n \t\t_, err = buildContext.ImportDir(path, 0)\n \t\tif err != nil {\n+\t\t\tif _, noGo := err.(*build.NoGoError); !noGo {\n+\t\t\t\tlog.Print(err)\n+\t\t\t}\n \t\t\treturn nil\n \t\t}\n \t\tpkgs = append(pkgs, name)\
@@ -520,8 +523,10 @@ func matchPackages(pattern string) []string {\n \t\t\t\treturn nil\n \t\t\t}\n \t\t\t_, err = buildContext.ImportDir(path, 0)\n-\t\t\tif err != nil && strings.Contains(err.Error(), \"no Go source files\") {\n-\t\t\t\treturn nil\n+\t\t\tif err != nil {\n+\t\t\t\tif _, noGo := err.(*build.NoGoError); noGo {\n+\t\t\t\t\treturn nil\n+\t\t\t\t}\n \t\t\t}\n \t\t\tpkgs = append(pkgs, name)\n \t\t\treturn nil\
@@ -588,6 +593,9 @@ func matchPackagesInFS(pattern string) []string {\n \t\t\treturn nil\n \t\t}\n \t\tif _, err = build.ImportDir(path, 0); err != nil {\n+\t\t\tif _, noGo := err.(*build.NoGoError); !noGo {\n+\t\t\t\tlog.Print(err)\n+\t\t\t}\n \t\t\treturn nil\n \t\t}\n \t\tpkgs = append(pkgs, name)\
具体的には、matchPackages
関数内の2箇所と、matchPackagesInFS
関数内の1箇所で、build.ImportDir
からのエラー処理ロジックが変更されています。
コアとなるコードの解説
変更の核となるのは、以下のパターンです。
if err != nil {
if _, noGo := err.(*build.NoGoError); !noGo {
log.Print(err)
}
return nil
}
このコードブロックは、build.ImportDir
がエラーを返した場合に実行されます。
if err != nil
: まず、ImportDir
がエラーを返したかどうかを確認します。if _, noGo := err.(*build.NoGoError); !noGo
: ここが重要な変更点です。err.(*build.NoGoError)
: 返されたエラーerr
がbuild.NoGoError
型であるかを型アサーションでチェックします。noGo
: 型アサーションの結果、err
がbuild.NoGoError
型であればtrue
、そうでなければfalse
がnoGo
変数に代入されます。!noGo
:noGo
がfalse
の場合、つまりエラーがbuild.NoGoError
型ではなかった場合に、内側のブロックが実行されます。
log.Print(err)
: エラーがbuild.NoGoError
以外のものであれば、そのエラーメッセージを標準エラー出力にログとして出力します。これにより、ユーザーは構文エラーなどの具体的な問題を把握できます。return nil
: エラーの種類に関わらず、該当するパッケージの処理を中断し、nil
を返します。これは、エラーが発生したパッケージをリストに含めないという従来の挙動を維持するためです。build.NoGoError
の場合も、Goソースファイルがないためパッケージとして扱わないという意図が継続されます。
以前のコードでは、strings.Contains(err.Error(), "no Go source files")
という文字列比較でbuild.NoGoError
を判断していました。これは、エラーメッセージの文字列に依存するため、将来的にエラーメッセージが変更された場合にコードが壊れる可能性がありました。型アサーションを使用することで、より堅牢でタイプセーフなエラーハンドリングが実現されています。
関連リンク
- Go CL 10568043: https://golang.org/cl/10568043
参考にした情報源リンク
- (特になし。コミットメッセージとコード差分から直接解析しました。)