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

[インデックス 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コマンドなどを用いてパッケージをスキャンする際、特定の条件下でエラーが発生しても、そのエラーがユーザーに通知されずにパッケージがリストから「サイレントに消える」という問題がありました。

具体的には、以下のようなシナリオが考えられます。

  1. 構文エラーのあるGoソースファイル: 例えば、import文に構文エラーがある場合など、Goのパーサーがファイルを正しく解析できないことがあります。
  2. build.ImportDirの挙動: buildパッケージのImportDir関数は、指定されたディレクトリからGoパッケージをインポートしようとします。この際、構文エラーなどによってインポートに失敗した場合、エラーを返しますが、そのエラーが上位のcmd/goコマンドで適切に処理されていませんでした。
  3. サイレントな無視: 以前の実装では、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 { ... }という構文は、以下の意味を持ちます。

  1. err.(*build.NoGoError): err*build.NoGoError型であるかをチェックします。
  2. noGo: 型アサーションが成功した場合(err*build.NoGoError型であった場合)、noGotrueになり、errの具象値が*build.NoGoError型として取り出されます(ここでは_で破棄されています)。失敗した場合はnoGofalseになります。
  3. !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してしまい、エラーメッセージがユーザーに表示されないという問題がありました。

新しい実装では、このエラーハンドリングがより堅牢になりました。

  1. 型アサーションによるエラーの識別: if _, noGo := err.(*build.NoGoError); !noGo { ... }という型アサーションを使用することで、エラーがbuild.NoGoError型であるかどうかを正確に判断するようになりました。文字列比較に比べて、より安全で意図が明確な方法です。
  2. NoGoErrorのエラーのログ出力: !noGo、つまりエラーがbuild.NoGoErrorではない場合(Goソースファイルが見つからない以外のエラー、例えば構文エラーなど)に、log.Print(err)を呼び出してエラーメッセージを標準エラー出力に表示するように変更されました。これにより、ユーザーはパッケージのインポートに失敗した具体的な理由を知ることができるようになりました。
  3. 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がエラーを返した場合に実行されます。

  1. if err != nil: まず、ImportDirがエラーを返したかどうかを確認します。
  2. if _, noGo := err.(*build.NoGoError); !noGo: ここが重要な変更点です。
    • err.(*build.NoGoError): 返されたエラーerrbuild.NoGoError型であるかを型アサーションでチェックします。
    • noGo: 型アサーションの結果、errbuild.NoGoError型であればtrue、そうでなければfalsenoGo変数に代入されます。
    • !noGo: noGofalseの場合、つまりエラーがbuild.NoGoError型ではなかった場合に、内側のブロックが実行されます。
  3. log.Print(err): エラーがbuild.NoGoError以外のものであれば、そのエラーメッセージを標準エラー出力にログとして出力します。これにより、ユーザーは構文エラーなどの具体的な問題を把握できます。
  4. return nil: エラーの種類に関わらず、該当するパッケージの処理を中断し、nilを返します。これは、エラーが発生したパッケージをリストに含めないという従来の挙動を維持するためです。build.NoGoErrorの場合も、Goソースファイルがないためパッケージとして扱わないという意図が継続されます。

以前のコードでは、strings.Contains(err.Error(), "no Go source files")という文字列比較でbuild.NoGoErrorを判断していました。これは、エラーメッセージの文字列に依存するため、将来的にエラーメッセージが変更された場合にコードが壊れる可能性がありました。型アサーションを使用することで、より堅牢でタイプセーフなエラーハンドリングが実現されています。

関連リンク

参考にした情報源リンク

  • (特になし。コミットメッセージとコード差分から直接解析しました。)