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

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

このコミットは、Goコマンドラインツール(cmd/go)におけるgo build -oコマンドの挙動を修正するものです。具体的には、go build -oコマンドに指定されたインポートパスパターンがどのパッケージにもマッチしなかった場合に発生していたパニック(panic)を修正し、代わりに警告メッセージを表示して非ゼロの終了コード(1)で終了するように変更します。これにより、ユーザーエクスペリエンスが向上し、スクリプトなどでの利用時に予期せぬパニックによる中断を防ぎます。

コミット

commit 3e801416ceadd622ad9703d52ac12065dba5eaf8
Author: Dmitri Shuralyov <shurcooL@gmail.com>
Date:   Wed Jul 9 13:17:27 2014 +1000

    cmd/go: fix build -o panic when import path pattern matches 0 pkgs
    
    Fixes #8165.
    
    After this change, the panic is replaced by a message:
    
            $ go build -o out ...doesntexist
            warning: "...doesntexist" matched no packages
            no packages to build
    
    The motivation to return 1 exit error code is to allow -o flag
    to be used to guarantee that the output binary is written to
    when exit status is 0. If someone uses an import path pattern
    to specify a single package and suddenly that matches no packages,
    it's better to return exit code 1 instead of silently doing nothing.
    This is consistent with the case when -o flag is given and multiple
    packages are matched.
    It's also somewhat consistent with the current behavior with the
    panic, except that gave return code 2. But it's similar in
    that it's also non-zero (indicating failure).
    I've changed the language to be similar to output of go test
    when an import path pattern matches no packages (it also has a return status of
    1):
    
            $ go test ...doesntexist
            warning: "...doesntexist" matched no packages
            no packages to test
    
    LGTM=adg
    R=golang-codereviews, josharian, gobot, adg
    CC=golang-codereviews
    https://golang.org/cl/107140043

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

https://github.com/golang/go/commit/3e801416ceadd622ad9703d52ac12065dba5eaf8

元コミット内容

cmd/go: fix build -o panic when import path pattern matches 0 pkgs

このコミットは、go build -oコマンドに指定されたインポートパスパターンがどのパッケージにもマッチしなかった場合に発生するパニックを修正します。

修正後、パニックは以下のメッセージに置き換えられます:

$ go build -o out ...doesntexist
warning: "...doesntexist" matched no packages
no packages to build

終了コード1を返す動機は、-oフラグが使用された場合に、終了ステータスが0であれば出力バイナリが書き込まれることを保証するためです。もし誰かが単一のパッケージを指定するためにインポートパスパターンを使用し、それが突然どのパッケージにもマッチしなくなった場合、何もせずにサイレントに終了するよりも、終了コード1を返す方が良いと判断されました。これは、-oフラグが与えられ、複数のパッケージがマッチした場合の挙動と一貫しています。また、パニックが発生した場合の現在の挙動(終了コード2を返す)ともある程度一貫しており、非ゼロ(失敗を示す)であるという点では似ています。

この変更は、インポートパスパターンがどのパッケージにもマッチしなかった場合のgo testの出力(これも終了ステータス1を返す)と類似するように言語が変更されています。

$ go test ...doesntexist
warning: "...doesntexist" matched no packages
no packages to test

変更の背景

このコミットが行われる前、Goのビルドコマンド(go build)に-oフラグ(出力ファイル名を指定するオプション)を付けて実行し、かつ指定されたインポートパスパターンがGoのワークスペース内でどのパッケージにもマッチしなかった場合、プログラムは予期せずパニックを起こして異常終了していました。

例えば、存在しないパッケージパスに対してgo build -o myapp ...doesntexistのようなコマンドを実行すると、Goツールは内部でパニック状態に陥り、スタックトレースを出力して終了していました。これは、コマンドラインツールとしては望ましくない挙動です。ユーザーは、コマンドが失敗した際に、何が問題だったのかを明確に理解できるエラーメッセージと、スクリプトなどから終了ステータスをチェックできる非ゼロの終了コードを期待します。パニックは開発者が予期しない内部エラーを示すものであり、ユーザーが入力したコマンドに対する通常の応答としては不適切です。

この問題はGoのIssue #8165として報告されており、このコミットはその問題を解決するために導入されました。変更の主な目的は、パニックをよりユーザーフレンドリーな警告メッセージと、スクリプトでの処理に適した非ゼロの終了コード(1)に置き換えることです。これにより、go buildコマンドの堅牢性と使いやすさが向上します。特に、-oフラグが指定されている場合、ユーザーは出力バイナリが生成されることを期待するため、パッケージが見つからない場合は明確な失敗を示すべきであるという設計思想が反映されています。

前提知識の解説

Go言語のビルドシステムとgo buildコマンド

Go言語は、ソースコードをコンパイルして実行可能なバイナリを生成するための強力なビルドシステムを内蔵しています。その中心となるのがgo buildコマンドです。

  • go build: Goのソースファイルやパッケージをコンパイルし、実行可能なバイナリを生成します。引数なしで実行すると、現在のディレクトリのパッケージをビルドします。特定のパッケージパスを指定することもできます。

パッケージとインポートパス

Goのコードは「パッケージ」という単位で整理されます。パッケージは関連する機能の集合であり、他のパッケージからインポートして利用できます。

  • インポートパス: パッケージを一意に識別するための文字列です。例えば、"fmt"は標準ライブラリのフォーマットパッケージを指し、"github.com/user/repo/mypkg"はGitHub上のリポジトリにあるパッケージを指します。
  • インポートパスパターン: ...(エリプシス)を含むことで、複数のパッケージにマッチするパターンを指定できます。例えば、./...は現在のディレクトリとそのサブディレクトリにあるすべてのパッケージを意味します。

-oフラグ

go buildコマンドには、生成される実行可能ファイルの出力パスとファイル名を指定するための-oフラグがあります。

  • go build -o <output_path> <package_path>: 指定された<package_path>のパッケージをビルドし、結果のバイナリを<output_path>に保存します。このフラグは通常、単一のパッケージをビルドする場合にのみ使用されます。複数のパッケージがマッチする場合、go build: cannot use -o with multiple packagesというエラーが発生します。

パニック (Panic)

Goにおける「パニック」は、プログラムの実行中に回復不可能なエラーが発生したことを示すメカニズムです。パニックが発生すると、通常のプログラムフローは中断され、遅延関数(defer)が実行された後、スタックトレースが出力されてプログラムが終了します。

  • パニックの発生: 配列の範囲外アクセス、nilポインタのデリファレンスなど、プログラマーの論理的な誤りや予期せぬ状態によって引き起こされることが多いです。
  • パニックの利用: 通常、パニックは回復不能なエラーを示すために使用され、アプリケーションの正常な動作を継続できない状況で発生させます。しかし、コマンドラインツールのようなユーザーと対話するプログラムでは、ユーザーの入力ミスなどによってパニックが発生するのは望ましくありません。このような場合は、明確なエラーメッセージと適切な終了コードを返す「エラー」として処理すべきです。

終了コード (Exit Code)

プログラムが終了する際に、オペレーティングシステムに対して返す数値です。

  • 0: プログラムが正常に終了したことを示します。
  • 非ゼロ: プログラムが何らかのエラーや異常な状態を伴って終了したことを示します。通常、1は一般的なエラー、2はコマンドライン引数の誤りなど、特定の意味を持つことがあります。スクリプトなどでは、この終了コードをチェックして、コマンドの成否を判断します。

このコミットの変更は、go build -oがパッケージを見つけられなかった場合に、パニックではなく、明確なエラーメッセージと終了コード1を返すことで、これらの前提知識に基づいたより堅牢でユーザーフレンドリーな挙動を実現しています。

技術的詳細

このコミットの技術的な変更は、src/cmd/go/build.goファイル内のrunBuild関数に集中しています。この関数は、go buildコマンドの主要なロジックを処理します。

変更前のコードでは、-oフラグが指定されている場合、ビルド対象のパッケージリスト(pkgs)の数がチェックされていました。

if *buildO != "" { // -o フラグが指定されている場合
    if len(pkgs) > 1 { // 複数のパッケージがマッチした場合
        fatalf("go build: cannot use -o with multiple packages")
    }
    // ここで len(pkgs) == 0 の場合の処理が欠落していた
    p := pkgs[0] // パッケージリストが空の場合、ここでインデックス範囲外アクセスによるパニックが発生していた
    p.target = "" // must build - not up to date
    // ...
}

問題は、len(pkgs) == 0、つまり指定されたインポートパスパターンがどのパッケージにもマッチしなかった場合の処理が明示的に存在しなかった点にあります。この場合、コードはpkgs[0]にアクセスしようとしますが、pkgsスライスは空であるため、Goランタイムは「インデックス範囲外アクセス」のエラーでパニックを起こしていました。

このコミットでは、この欠落していたケースを明示的に処理するためのelse ifブロックが追加されました。

if *buildO != "" {
    if len(pkgs) > 1 {
        fatalf("go build: cannot use -o with multiple packages")
    } else if len(pkgs) == 0 { // 新しく追加された条件
        fatalf("no packages to build") // エラーメッセージを出力し、終了コード1で終了
    }
    p := pkgs[0]
    p.target = "" // must build - not up to date
    // ...
}

fatalf関数の役割

fatalfは、Goコマンドラインツール内でエラーメッセージを出力し、非ゼロの終了コードでプログラムを終了させるためのヘルパー関数です。この関数が呼び出されると、指定されたフォーマット文字列と引数を使ってエラーメッセージが標準エラー出力に書き込まれ、その後、プログラムは終了コード1(一般的なエラーを示す)で終了します。これにより、パニックのような予期せぬ終了ではなく、制御されたエラー終了が実現されます。

終了コード1の選択と一貫性

コミットメッセージで述べられているように、終了コード1を返すことにはいくつかの動機があります。

  1. -oフラグの保証: -oフラグは、ビルドが成功した場合に特定のパスにバイナリが生成されることをユーザーに保証するものです。もしパッケージが見つからずにサイレントに終了したり、パニックを起こしたりすると、この保証が破られます。終了コード1を返すことで、ビルドが期待通りに完了しなかったことを明確に示し、スクリプトなどでの自動処理においてエラーハンドリングを可能にします。
  2. go testとの一貫性: go testコマンドも、インポートパスパターンがどのテストパッケージにもマッチしなかった場合に、同様の警告メッセージと終了コード1を返します。この変更により、go buildgo testというGoの主要なコマンドラインツールの挙動に一貫性がもたらされ、ユーザーはより予測可能な体験を得られます。
  3. パニックからの移行: 以前のパニックは終了コード2を返していましたが、これは非ゼロであるため失敗を示していました。しかし、パニックは内部エラーを示すものであり、ユーザー入力に対する応答としては不適切です。終了コード1は一般的なエラーを示すため、より適切です。

この変更は、Goコマンドラインツールの堅牢性とユーザーフレンドリーさを向上させるための、細部ながらも重要な改善です。

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

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -287,6 +287,8 @@ func runBuild(cmd *Command, args []string) {
 	if *buildO != "" {
 		if len(pkgs) > 1 {
 			fatalf("go build: cannot use -o with multiple packages")
+		} else if len(pkgs) == 0 {
+			fatalf("no packages to build")
 		}
 		p := pkgs[0]
 		p.target = "" // must build - not up to date

コアとなるコードの解説

変更はsrc/cmd/go/build.goファイルのrunBuild関数内、具体的には-oフラグが指定された場合の処理ブロックに追加されています。

  1. else if len(pkgs) == 0 {: この行は、if len(pkgs) > 1(複数のパッケージがマッチした場合)の条件に続くelse ifブロックとして追加されました。これは、pkgsスライス(ビルド対象のパッケージのリスト)の長さが0である、つまり指定されたインポートパスパターンがどのパッケージにもマッチしなかった場合にこのブロック内のコードが実行されることを意味します。

  2. fatalf("no packages to build"): この行は、上記の条件が真であった場合に実行されます。

    • fatalf関数は、Goコマンドラインツール内でエラーメッセージを標準エラー出力に表示し、プログラムを非ゼロの終了コード(通常は1)で終了させるためのユーティリティ関数です。
    • 引数として渡された文字列 "no packages to build"がエラーメッセージとして表示されます。

この2行の追加により、以前はpkgs[0]へのアクセスでパニックが発生していた状況が、明確なエラーメッセージの表示と、スクリプトなどでのエラーハンドリングを可能にする非ゼロの終了コードでの正常な終了に置き換えられました。これにより、go build -oコマンドの堅牢性とユーザーエクスペリエンスが大幅に向上しています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(go buildコマンド、パッケージ、パニックに関する一般的な情報)
  • Goのソースコード(src/cmd/go/build.go
  • 本コミットのメッセージと差分情報
  • Google検索: "Go issue 8165 go build -o panic" (関連する可能性のあるIssueの確認のため)