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

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

このコミットは、Goコマンドラインツール(cmd/go)におけるエラーハンドリングの改善を目的としています。具体的には、テスト専用のパッケージ(Goのソースコードを含まず、テストファイルのみを含むパッケージ)をgo installしようとした際に、より分かりやすいエラーメッセージを返すように変更が加えられました。これにより、ユーザーが意図しない操作を行った場合に、その原因を迅速に特定できるようになります。

コミット

commit f409681925407db843d4c9e314ca9edadd6a625b
Author: Russ Cox <rsc@golang.org>
Date:   Mon May 12 11:04:28 2014 -0400

    cmd/go: better error for install of 'test-only' package
    
    Fixes #7915.
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/96210044

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

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

元コミット内容

cmd/go: テスト専用パッケージのインストールに対するより良いエラーメッセージ

Issue #7915 を修正。

変更の背景

Goのパッケージ管理システムでは、通常のGoソースファイル(.goファイル)とテストファイル(_test.goファイル)は区別されます。テストファイルは、パッケージのテストを実行するためにのみ使用され、通常はパッケージのビルドやインストール時には含まれません。

このコミットが修正しようとした問題は、Goのパッケージが通常のGoソースファイルを含まず、テストファイル(_test.go)のみを含む場合、つまり「テスト専用パッケージ」である場合に発生しました。このようなパッケージに対してgo installコマンドを実行すると、以前のバージョンでは曖昧なエラーメッセージが表示されるか、あるいは期待通りに動作しない可能性がありました。

具体的には、go installは実行可能なバイナリやライブラリをビルドしてインストールすることを目的としていますが、テスト専用パッケージにはビルド対象となる通常のGoソースコードが存在しません。このため、go installが失敗した場合に、ユーザーがその原因を正確に理解し、適切な対処を行うことが困難でした。

このコミットは、このような状況でgo installが実行された際に、より明確で分かりやすいエラーメッセージ(例:「no buildable Go source files in ...」)を返すようにすることで、ユーザーエクスペリエンスを向上させることを目的としています。これにより、ユーザーはテスト専用パッケージをインストールしようとしていることに気づき、go testなどの適切なコマンドを使用するように促されます。

また、この変更はGoのコードレビューシステム(Gerrit)におけるCL 96210044として提出され、Russ Coxによって作成され、Brad Fitzpatrickによってレビューされました。コミット後、darwin-amd64-race-cheneyビルダでビルドが失敗するという問題が発生しましたが、これは後続のコミット(CL 100410044)で修正されました。これは、変更が意図しない副作用を引き起こす可能性があり、継続的なテストと監視が重要であることを示しています。

前提知識の解説

  • Goパッケージ: Go言語では、関連するソースファイルの集まりをパッケージと呼びます。パッケージは、importパスによって識別され、再利用可能なコードの単位となります。
  • Goソースファイルとテストファイル:
    • 通常のGoソースファイル(例: main.go, util.go): パッケージの主要なロジックを実装します。
    • テストファイル(例: my_package_test.go): ファイル名が_test.goで終わるファイルで、パッケージのテストコードを含みます。これらのファイルは、go buildgo installの際には通常無視され、go testコマンドによってのみコンパイル・実行されます。
  • go buildコマンド: Goソースファイルをコンパイルして実行可能なバイナリやパッケージを生成します。
  • go installコマンド: go buildと同様にコンパイルを行いますが、生成されたバイナリやパッケージをGOPATH/binまたはGOBINにインストールします。
  • build.NoGoError: Goのビルドシステム内で定義されているエラータイプの一つで、ビルド対象のディレクトリにGoのソースファイルが見つからない場合に発生します。このコミットでは、このエラーをより適切なタイミングで返すように変更されています。
  • cmd/go: Go言語の公式ツールチェーンに含まれるコマンドラインツールで、Goプログラムのビルド、テスト、インストール、フォーマットなど、様々な操作を行います。

技術的詳細

このコミットの主要な変更は、src/cmd/go/build.goファイル内のbuilder.buildメソッドにあります。このメソッドは、Goパッケージをビルドする際の中心的なロジックを担っています。

変更前は、gofiles(通常のGoソースファイルのリスト)の長さが0より大きい場合にのみ、Goコンパイラ(buildToolchain.gc)を呼び出す条件分岐がありました。しかし、この条件では、テストファイルのみを含むパッケージの場合に、gofilesが空になり、コンパイラが呼び出されないまま処理が進んでしまう可能性がありました。この場合、最終的にビルドが失敗しても、その原因が「Goソースファイルがないため」であることが明確に伝わらない可能性がありました。

このコミットでは、以下の2つの主要な変更が行われました。

  1. build.NoGoErrorの早期検出と返却: builder.buildメソッドの冒頭近くに、gofilesの長さが0であるかをチェックする新しい行が追加されました。

    if len(gofiles) == 0 {
        return &build.NoGoError{a.p.Dir}
    }
    

    この変更により、パッケージ内にビルド可能なGoソースファイルが一つも存在しない場合(つまり、テストファイルのみが存在する場合など)、Goコンパイラを呼び出す前に、明示的にbuild.NoGoErrorが返されるようになりました。これにより、エラーメッセージがより具体的になり、ユーザーは「Goソースファイルがない」という明確な理由を知ることができます。

  2. buildToolchain.gc呼び出しの条件分岐の削除: 以前はif len(gofiles) > 0という条件で囲まれていたbuildToolchain.gcの呼び出しが、この条件分岐から外されました。

    // 変更前:
    // if len(gofiles) > 0 {
    //     ofile, out, err := buildToolchain.gc(b, a.p, a.objpkg, obj, inc, gofiles)
    //     // ...
    // }
    
    // 変更後:
    ofile, out, err := buildToolchain.gc(b, a.p, a.objpkg, obj, inc, gofiles)
    // ...
    

    この変更は、上記1の変更と組み合わさることで、よりロジックを簡潔にしています。gofilesが空の場合でもbuildToolchain.gcが呼び出されることになりますが、その前にbuild.NoGoErrorが返されるため、実際にはこのパスは実行されません。これにより、コードの可読性が向上し、冗長なチェックが排除されました。

また、この変更の動作を検証するために、src/cmd/go/test.bashに新しいテストケースが追加されました。このテストケースは、testdata/testonlyという新しいディレクトリに配置された、テストファイル(p_test.go)のみを含むパッケージに対してgo buildを実行し、期待されるエラーメッセージ(no buildable Go)が返されることを確認します。

src/cmd/go/testdata/testonly/p_test.goは、package pという非常にシンプルな内容で、通常のGoソースファイルを含まない「テスト専用パッケージ」の最小限の例として機能します。

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

src/cmd/go/build.go

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -882,6 +882,10 @@ func (b *builder) build(a *action) (err error) {
 		gofiles = append(gofiles, outGo...)
 	}
 
+	if len(gofiles) == 0 {
+		return &build.NoGoError{a.p.Dir}
+	}
+
 	// If we're doing coverage, preprocess the .go files and put them in the work directory
 	if a.p.coverMode != "" {
 		for i, file := range gofiles {
@@ -915,21 +919,19 @@ func (b *builder) build(a *action) (err error) {
 	inc := b.includeArgs("-I", a.deps)
 
 	// Compile Go.
-	if len(gofiles) > 0 {
-		ofile, out, err := buildToolchain.gc(b, a.p, a.objpkg, obj, inc, gofiles)
-		if len(out) > 0 {
-			b.showOutput(a.p.Dir, a.p.ImportPath, b.processOutput(out))
-			if err != nil {
-				return errPrintedOutput
-			}
-		}
+	ofile, out, err := buildToolchain.gc(b, a.p, a.objpkg, obj, inc, gofiles)
+	if len(out) > 0 {
+		b.showOutput(a.p.Dir, a.p.ImportPath, b.processOutput(out))
 	if err != nil {
-		return err
-	}
-	if ofile != a.objpkg {
-		objects = append(objects, ofile)
+		return errPrintedOutput
 	}
+	}
+	if err != nil {
+		return err
+	}
+	if ofile != a.objpkg {
+		objects = append(objects, ofile)
+	}
 
 	// Copy .h files named for goos or goarch or goos_goarch
 	// to names using GOOS and GOARCH.

src/cmd/go/test.bash

--- a/src/cmd/go/test.bash
+++ b/src/cmd/go/test.bash
@@ -759,6 +759,17 @@ fi
 rm -rf $d
 unset GOPATH
 
+TEST 'go build in test-only directory fails with a good error'
+if ./testgo build ./testdata/testonly 2>testdata/err.out; then
+	echo "go build ./testdata/testonly succeeded, should have failed"
+	ok=false
+elif ! grep 'no buildable Go' testdata/err.out >/dev/null; then
+	echo "go build ./testdata/testonly produced unexpected error:"
+	cat testdata/err.out
+	ok=false
+fi
+rm -f testdata/err.out
+
 # clean up
 if $started; then stop; fi
 rm -rf testdata/bin testdata/bin1

src/cmd/go/testdata/testonly/p_test.go

--- /dev/null
+++ b/src/cmd/go/testdata/testonly/p_test.go
@@ -0,0 +1 @@
+package p

コアとなるコードの解説

src/cmd/go/build.goの変更

  • if len(gofiles) == 0 { return &build.NoGoError{a.p.Dir} }: この行が追加されたことで、builder.build関数は、パッケージ内にコンパイル対象となるGoソースファイル(gofiles)が一つも存在しない場合、すぐにbuild.NoGoErrorを返します。a.p.Dirは、現在処理しているパッケージのディレクトリパスを示しており、エラーメッセージに含めることで、どのディレクトリに問題があるのかをユーザーに明確に伝えます。これにより、例えば_test.goファイルしか含まないディレクトリに対してgo buildgo installを実行した場合に、より適切なエラーメッセージが表示されるようになります。

  • buildToolchain.gc呼び出しの条件分岐の削除: 以前はif len(gofiles) > 0という条件で囲まれていたbuildToolchain.gc(Goコンパイラを呼び出す部分)が、条件分岐なしで直接呼び出されるようになりました。これは、上記のlen(gofiles) == 0のチェックが追加されたため、gofilesが空である場合には既にエラーが返されているため、この条件分岐が不要になったためです。これにより、コードがよりシンプルになり、冗長なチェックが排除されました。

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

  • 新しいテストケースの追加: このシェルスクリプトには、新しいTESTブロックが追加されました。これは、go build ./testdata/testonlyコマンドを実行し、その出力(標準エラー出力)をtestdata/err.outにリダイレクトします。
    • if ./testgo build ./testdata/testonly 2>testdata/err.out; then ...testgoはGoテストフレームワークの一部で、Goコマンドのテスト版を実行します。このコマンドが成功した場合(つまり、エラーを返さなかった場合)、それは予期せぬ動作であるため、テストは失敗とマークされます。
    • elif ! grep 'no buildable Go' testdata/err.out >/dev/null; then ...testgoコマンドがエラーを返した場合、そのエラー出力(testdata/err.out)に「no buildable Go」という文字列が含まれているかをgrepで確認します。この文字列が含まれていない場合、それは期待されるエラーメッセージではないため、テストは失敗とマークされます。
    • rm -f testdata/err.out:テスト後に一時ファイルtestdata/err.outを削除します。 このテストケースは、build.goの変更が意図通りに機能し、「テスト専用パッケージ」に対してgo buildが実行された際に、適切なエラーメッセージが返されることを保証します。

src/cmd/go/testdata/testonly/p_test.goの追加

  • テスト専用パッケージの定義: このファイルは、package pという非常にシンプルな内容で構成されています。ファイル名が_test.goで終わるため、Goツールチェーンによってテストファイルとして認識されます。このディレクトリには他の.goファイルが存在しないため、このディレクトリは「テスト専用パッケージ」として機能し、build.goの変更をテストするための具体的なシナリオを提供します。

これらの変更により、Goツールチェーンは、テスト専用パッケージのビルドやインストールに関するユーザーの誤解を減らし、より明確なフィードバックを提供するようになりました。

関連リンク

参考にした情報源リンク