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

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

このコミットは、Go言語のビルドシステムにおいて、go install コマンドなどでパッケージが見つからなかった場合に表示されるエラーメッセージを改善するものです。具体的には、「cannot find package」エラーが発生した際に、Goがパッケージを検索したパス($GOROOT および $GOPATH 内のパス)をより詳細に、かつ分かりやすく表示するように変更されています。これにより、ユーザーはパッケージが見つからない原因を特定しやすくなります。

コミット

commit 11d96dd7f51cf52f6cfea14e4123c21e75a3ff74
Author: Dave Cheney <dave@cheney.net>
Date:   Wed Dec 12 21:38:52 2012 +1100

    go/build: give better explanation for "cannot find package"
    
    Fixes #4079.
    
    Some example output:
    
    % go install foo/bar
    can't load package: package foo/bar: cannot find package "foo/bar" in any of:
            /home/dfc/go/src/pkg/foo/bar (from $GOROOT)
            /home/dfc/src/foo/bar (from $GOPATH)
            /home/dfc/src2/src/foo/bar
    
    % GOPATH= go install foo/bar
    can't load package: package foo/bar: cannot find package "foo/bar" in any of:
            /home/dfc/go/src/pkg/foo/bar (from $GOROOT)
            ($GOPATH not set)
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6899057

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

https://github.com/golang/go/commit/11d96dd7f51cf52f6cfea14e4123c21e75a3ff74

元コミット内容

このコミットの目的は、go/build パッケージが「cannot find package」エラーを報告する際に、より詳細な情報を提供することです。具体的には、Goがパッケージを検索したすべてのパスを列挙し、それぞれのパスが $GOROOT または $GOPATH のどこから来たのかを示すことで、ユーザーが問題の原因を特定しやすくします。また、$GOPATH が設定されていない場合にはその旨を明示するようになります。

変更の背景

Go言語の初期のバージョンでは、go installgo build などのコマンドで指定されたパッケージが見つからない場合、単に「cannot find package」というエラーメッセージが表示されるだけでした。このメッセージだけでは、ユーザーはGoがどのパスを検索し、なぜパッケージが見つからなかったのかを理解するのが困難でした。例えば、タイプミス、環境変数の設定ミス、あるいはパッケージが正しい場所に配置されていないなど、様々な原因が考えられますが、エラーメッセージからはその手がかりを得ることができませんでした。

この問題は、GoのIssue #4079として報告されており、ユーザーエクスペリエンスの改善が求められていました。開発者は、エラーメッセージに検索パスを含めることで、ユーザーがデバッグにかける時間を大幅に削減できると考えました。特に、$GOPATH の設定はGo開発において頻繁に問題となる点であり、その情報がエラーメッセージに含まれることは非常に有用です。

前提知識の解説

このコミットの変更を理解するためには、Go言語のパッケージ管理と環境変数に関する以下の知識が必要です。

Go言語のパッケージとモジュール

Go言語では、コードは「パッケージ」という単位で管理されます。関連する機能は同じパッケージにまとめられ、他のパッケージからインポートして利用されます。Go 1.11以降は「Go Modules」が導入され、依存関係管理がより現代的になりましたが、このコミットが作成された2012年当時は、$GOPATH ベースのワークスペースが主流でした。

$GOROOT

$GOROOT は、Go言語のSDK(Standard Development Kit)がインストールされているディレクトリのパスを指す環境変数です。Goの標準ライブラリパッケージ(fmt, net/http など)は、この $GOROOT/src ディレクトリ以下に配置されています。Goのビルドツールは、まず $GOROOT 内で指定されたパッケージを検索します。

$GOPATH

$GOPATH は、ユーザーが開発するGoプロジェクトのワークスペースのルートディレクトリを指定する環境変数です。Go 1.11でGo Modulesが導入されるまでは、Goのプロジェクトは $GOPATH の下に src, pkg, bin というサブディレクトリを持つ構造が一般的でした。

  • src: ソースコードが配置される場所。インポートパスは $GOPATH/src からの相対パスで解決されます。
  • pkg: コンパイルされたパッケージオブジェクトが配置される場所。
  • bin: コンパイルされた実行可能ファイルが配置される場所。

$GOPATH は複数のパスをコロン(Unix/Linux)またはセミコロン(Windows)で区切って指定することができます。Goのビルドツールは、$GOROOT の次に $GOPATH に指定された各パスの src ディレクトリ以下を検索します。

go install コマンド

go install コマンドは、Goのソースコードをコンパイルし、実行可能ファイルまたはパッケージオブジェクトを生成して、それぞれ $GOPATH/bin または $GOPATH/pkg にインストールするコマンドです。このコマンドは、指定されたパッケージの依存関係を解決するために、上述の $GOROOT$GOPATH を参照してパッケージを検索します。

技術的詳細

このコミットの技術的な変更は、主に src/pkg/go/build/build.go ファイル内の Context.Import メソッドに集中しています。このメソッドは、Goのビルドプロセスにおいて、指定されたインポートパスに対応するパッケージを見つける役割を担っています。

変更前は、パッケージが見つからなかった場合、単に fmt.Errorf("import %q: cannot find package", path) という汎用的なエラーを返していました。

変更後は、パッケージの検索に失敗した場合に、Goが実際にどのパスを検索したのか、そしてそれぞれのパスが $GOROOT または $GOPATH のどのエントリに対応するのかを記録する新しいロジックが追加されました。

具体的には、以下の点が変更されています。

  1. 検索パスの記録:

    • tried という匿名構造体が導入され、goroot ( $GOROOT 内の検索パス) と gopath ( $GOPATH 内の検索パスのリスト) を記録するようになりました。
    • $GOROOT 内のパスを検索し、見つからなかった場合は tried.goroot にそのパスを保存します。
    • $GOPATH 内の各パスを検索し、見つからなかった場合は tried.gopath にそのパスを順次追加します。
  2. エラーメッセージの生成:

    • パッケージが見つからなかった場合、tried 構造体に記録された情報を使用して、詳細なエラーメッセージを構築します。
    • $GOROOT からの検索パスは (from $GOROOT) という注釈付きで表示されます。
    • $GOPATH からの検索パスは、最初のパスに (from $GOPATH) という注釈が付き、それ以降のパスには注釈が付きません。これは、$GOPATH が複数のエントリを持つ場合に、どのエントリが参照されたかを明確にするためです。
    • $GOPATH が設定されていない場合、または $GOROOT が設定されていない場合には、それぞれ ($GOPATH not set) または ($GOROOT not set) と表示されます。
    • これらのパスは改行で区切られ、最終的に cannot find package %q in any of:\n%s というフォーマットでエラーメッセージが生成されます。

これにより、ユーザーはエラーメッセージを見るだけで、Goがどのディレクトリを検索し、なぜ目的のパッケージが見つからなかったのかを正確に把握できるようになります。

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

src/pkg/go/build/build.go

--- a/src/pkg/go/build/build.go
+++ b/src/pkg/go/build/build.go
@@ -424,6 +424,13 @@ func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Packa
 		if strings.HasPrefix(path, "/") {
 			return p, fmt.Errorf("import %q: cannot import absolute path", path)
 		}
+
+		// tried records the location of unsucsessful package lookups
+		var tried struct {
+			goroot string
+			gopath []string
+		}
+
 		// Determine directory from import path.
 		if ctxt.GOROOT != "" {
 			dir := ctxt.joinPath(ctxt.GOROOT, "src", "pkg", path)
@@ -435,6 +442,7 @@ func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Packa
 				p.Root = ctxt.GOROOT
 				goto Found
 			}
+			tried.goroot = dir
 		}
 		for _, root := range ctxt.gopath() {
 			dir := ctxt.joinPath(root, "src", path)
@@ -445,8 +453,28 @@ func (ctxt *Context) Import(path string, srcDir string, mode ImportMode) (*Packa
 				p.Root = root
 				goto Found
 			}
+			tried.gopath = append(tried.gopath, dir)
+		}
+
+		// package was not found
+		var paths []string
+		if tried.goroot != "" {
+			paths = append(paths, fmt.Sprintf("\t%s (from $GOROOT)", tried.goroot))
+		} else {
+			paths = append(paths, "\t($GOROOT not set)")
+		}
+		var i int
+		var format = "\t%s (from $GOPATH)"
+		for ; i < len(tried.gopath); i++ {
+			if i > 0 {
+				format = "\t%s"
+			}
+			paths = append(paths, fmt.Sprintf(format, tried.gopath[i]))
+		}
+		if i == 0 {
+			paths = append(paths, "\t($GOPATH not set)")
 		}
-		return p, fmt.Errorf("import %q: cannot find package", path)
+		return p, fmt.Errorf("cannot find package %q in any of:\n%s", path, strings.Join(paths, "\n"))
 	}
 
 Found:

src/cmd/go/test.bash

このファイルでは、新しいエラーメッセージの出力形式を検証するためのテストケースが追加されています。

--- a/src/cmd/go/test.bash
+++ b/src/cmd/go/test.bash
@@ -150,6 +150,37 @@ if ! ./testgo list std | cmp -s test_std.list - ; then
 fi
 rm -f test_std.list
 
+# issue 4096. Validate the output of unsucessful go install foo/quxx 
+if [ $(./testgo install 'foo/quxx' 2>&1 | grep -c 'cannot find package "foo/quxx" in any of') -ne 1 ] ; then
+	echo 'go install foo/quxx expected error: .*cannot find package "foo/quxx" in any of'
+	ok=false
+fi 
+# test GOROOT search failure is reported
+if [ $(./testgo install 'foo/quxx' 2>&1 | egrep -c 'foo/quxx \(from \$GOROOT\)$') -ne 1 ] ; then
+        echo 'go install foo/quxx expected error: .*foo/quxx (from $GOROOT)'
+        ok=false
+fi
+# test multiple GOPATH entries are reported separately
+if [ $(GOPATH=$(pwd)/testdata/a:$(pwd)/testdata/b ./testgo install 'foo/quxx' 2>&1 | egrep -c 'testdata/./src/foo/quxx') -ne 2 ] ; then
+        echo 'go install foo/quxx expected error: .*testdata/a/src/foo/quxx (from $GOPATH)\n.*testdata/b/src/foo/quxx'
+        ok=false
+fi
+# test (from $GOPATH) annotation is reported for the first GOPATH entry
+if [ $(GOPATH=$(pwd)/testdata/a:$(pwd)/testdata/b ./testgo install 'foo/quxx' 2>&1 | egrep -c 'testdata/a/src/foo/quxx \(from \$GOPATH\)$') -ne 1 ] ; then
+        echo 'go install foo/quxx expected error: .*testdata/a/src/foo/quxx (from $GOPATH)'
+        ok=false
+fi
+# but not on the second
+if [ $(GOPATH=$(pwd)/testdata/a:$(pwd)/testdata/b ./testgo install 'foo/quxx' 2>&1 | egrep -c 'testdata/b/src/foo/quxx$') -ne 1 ] ; then
+        echo 'go install foo/quxx expected error: .*testdata/b/src/foo/quxx'
+        ok=false
+fi
+# test missing GOPATH is reported
+if [ $(GOPATH= ./testgo install 'foo/quxx' 2>&1 | egrep -c '\(\$GOPATH not set\)$') -ne 1 ] ; then
+        echo 'go install foo/quxx expected error: ($GOPATH not set)'
+        ok=false
+fi
+
 # clean up
 rm -rf testdata/bin testdata/bin1
 rm -f testgo

コアとなるコードの解説

src/pkg/go/build/build.go の変更点

  1. tried 構造体の導入:

    		var tried struct {
    			goroot string
    			gopath []string
    		}
    

    この匿名構造体は、パッケージ検索の試行結果を一時的に保持するために導入されました。goroot$GOROOT 内で試行されたパスを、gopath$GOPATH 内で試行されたパスのリストを格納します。

  2. tried.goroot へのパスの記録:

    			p.Root = ctxt.GOROOT
    			goto Found
    		}
    		tried.goroot = dir // パッケージが見つからなかった場合、試行されたGOROOTパスを記録
    	}
    

    $GOROOT 内でパッケージが見つからなかった場合、検索を試みたディレクトリパス (dir) が tried.goroot に保存されます。

  3. tried.gopath へのパスの記録:

    			p.Root = root
    			goto Found
    		}
    		tried.gopath = append(tried.gopath, dir) // パッケージが見つからなかった場合、試行されたGOPATHパスを記録
    	}
    

    $GOPATH 内の各エントリでパッケージが見つからなかった場合、検索を試みたディレクトリパス (dir) が tried.gopath スライスに追加されます。

  4. 詳細なエラーメッセージの生成:

    		// package was not found
    		var paths []string
    		if tried.goroot != "" {
    			paths = append(paths, fmt.Sprintf("\t%s (from $GOROOT)", tried.goroot))
    		} else {
    			paths = append(paths, "\t($GOROOT not set)")
    		}
    		var i int
    		var format = "\t%s (from $GOPATH)"
    		for ; i < len(tried.gopath); i++ {
    			if i > 0 {
    				format = "\t%s"
    			}
    			paths = append(paths, fmt.Sprintf(format, tried.gopath[i]))
    		}
    		if i == 0 {
    			paths = append(paths, "\t($GOPATH not set)")
    		}
    		return p, fmt.Errorf("cannot find package %q in any of:\n%s", path, strings.Join(paths, "\n"))
    

    この部分が、新しいエラーメッセージを構築する核心です。

    • paths という文字列スライスが初期化され、最終的なエラーメッセージの各行を格納します。
    • tried.goroot が空でなければ、$GOROOT からの検索パスが (from $GOROOT) という注釈付きで追加されます。空の場合($GOROOT が設定されていないか、検索対象外だった場合)は ($GOROOT not set) が追加されます。
    • $GOPATH の各エントリについてループ処理が行われます。最初の $GOPATH エントリには (from $GOPATH) という注釈が付き、それ以降のエントリには注釈が付きません。これにより、$GOPATH が複数のパスを持つ場合に、どのパスが $GOPATH の一部であるかを明確に示します。
    • tried.gopath が空の場合($GOPATH が設定されていないか、検索対象外だった場合)は ($GOPATH not set) が追加されます。
    • 最後に、fmt.Errorf を使用して、指定されたパッケージ名と、paths スライスを改行で結合した文字列を含むエラーメッセージを生成して返します。

src/cmd/go/test.bash の変更点

このシェルスクリプトは、Goコマンドのテストスイートの一部です。追加されたテストケースは、go install コマンドがパッケージを見つけられなかった場合に、期待されるエラーメッセージが正しく出力されることを検証します。

  • grep -c 'cannot find package "foo/quxx" in any of':新しいエラーメッセージの冒頭部分が正しく含まれているかを確認します。
  • egrep -c 'foo/quxx \(from \$GOROOT\)$'$GOROOT からの検索パスが正しく表示され、(from $GOROOT) という注釈が付いているかを確認します。
  • GOPATH=$(pwd)/testdata/a:$(pwd)/testdata/b ./testgo install 'foo/quxx':複数の $GOPATH エントリを設定し、それぞれのパスがエラーメッセージに含まれることを検証します。
  • egrep -c 'testdata/a/src/foo/quxx \(from \$GOPATH\)$':最初の $GOPATH エントリに (from $GOPATH) という注釈が付いていることを確認します。
  • egrep -c 'testdata/b/src/foo/quxx$':2番目以降の $GOPATH エントリには注釈が付かないことを確認します。
  • GOPATH= ./testgo install 'foo/quxx'$GOPATH が設定されていない場合に ($GOPATH not set) が表示されることを確認します。

これらのテストケースは、go/build パッケージの変更が意図通りに機能し、ユーザーに分かりやすいエラーメッセージを提供していることを保証します。

関連リンク

参考にした情報源リンク

  • コミットメッセージおよび変更されたソースコードの内容
  • Go言語の公式ドキュメント(GOROOT, GOPATH, パッケージ管理に関する一般的な知識)
  • Go言語のビルドプロセスに関する一般的な理解