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

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

このコミットは、Go言語のツールチェインの一部である cmd/go コマンドに関連する src/cmd/go/pkg.go ファイルに対する変更です。pkg.go ファイルは、Goパッケージのビルドプロセスにおけるパッケージ情報のロード、依存関係の解決、およびエラー処理、特にインポートサイクルの検出と診断に関するロジックを扱っています。

コミット

このコミットは、Go言語のビルドツール cmd/go におけるインポートサイクルの診断機能を改善することを目的としています。以前は、インポートサイクルが発生した際に、そのサイクルを構成するインポートパス(インポートスタック)が適切に表示されず、またビルドの後続フェーズで情報が上書きされてしまう問題がありました。この変更により、インポートサイクルエラー発生時に、より明確で役立つインポートスタック情報がエラーメッセージとして出力されるようになります。

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

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

元コミット内容

cmd/go: diagnose import cycles better

Before this CL, the import stack was a) not printed and b) overwritten later
in the build, destroying the information about the cycle. This CL fixes both.

I made time depend on os (os already depends on time) and with this CL the error is:

/Users/r/go/src/pkg/fmt/print.go:10:2: import cycle not allowed
package code.google.com/p/XXX/YYY:
        imports fmt
        imports os
        imports time
        imports os

Doesn't give line numbers for the actual imports, as requested in the bug, but
I don't believe that's important.

Fixes #4292.

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/13100043

変更の背景

この変更は、Go言語のビルドプロセスにおいて、パッケージ間のインポートサイクルが検出された際のエラーメッセージの質を向上させるために行われました。元のシステムでは、インポートサイクルが発生しても、そのサイクルを形成する具体的なインポートパス(どのパッケージがどのパッケージをインポートしているかという連鎖)がエラーメッセージに表示されませんでした。さらに、ビルドの進行中にこの重要な情報が上書きされてしまい、デバッグが困難になるという問題がありました。

この問題は、Go issue #4292 として報告されており、ユーザーはインポートサイクルの原因を特定するために、より詳細な情報、特にインポートパスの表示を求めていました。このコミットは、これらの問題を解決し、開発者がインポートサイクルをより迅速に特定し、修正できるようにすることを目的としています。コミットメッセージの例では、fmt -> os -> time -> os というインポートサイクルが明確に示されており、これが以前は得られなかった情報です。

前提知識の解説

Go言語のパッケージ管理とインポートシステム

Go言語は、コードをパッケージという単位で整理します。各パッケージは、関連する機能の集合体であり、他のパッケージから関数、変数、型などをインポートして利用することができます。インポートは import "path/to/package" の形式で記述され、Goコンパイラはこれらのインポート宣言を解析して、プログラム全体の依存関係グラフを構築します。

インポートサイクルとは何か、なぜそれが問題なのか

インポートサイクル(Import Cycle)とは、複数のパッケージが互いに直接的または間接的に依存し合っている状態を指します。例えば、パッケージAがパッケージBをインポートし、パッケージBがパッケージAをインポートしている場合(A -> B -> A)、これは直接的なインポートサイクルです。より複雑なケースとして、A -> B -> C -> A のように、複数のパッケージを介して循環的な依存関係が形成されることもあります。

インポートサイクルは、Go言語では許可されていません。その理由は以下の通りです。

  1. コンパイルの困難さ: 循環的な依存関係があると、どのパッケージを最初にコンパイルすべきか決定できません。コンパイラは依存関係の順序でパッケージをビルドする必要があるため、サイクルがあるとビルド順序を確立できません。
  2. コードの結合度の高さ: インポートサイクルは、パッケージ間の結合度が高いことを示唆しています。これは、モジュール性や再利用性を損ない、コードの理解や変更を困難にします。
  3. 初期化順序の問題: Go言語では、パッケージの init 関数が依存関係の順序で実行されます。インポートサイクルがあると、init 関数の実行順序が不定になり、予期せぬ動作を引き起こす可能性があります。

cmd/go ツールの役割

cmd/go は、Go言語の公式ツールチェインの中核をなすコマンドラインツールです。Goプログラムのビルド、テスト、インストール、依存関係の管理など、開発者が日常的に行う多くのタスクを処理します。パッケージの依存関係を解決し、インポートサイクルなどの問題を検出するのも cmd/go の重要な役割の一つです。

PackageError 構造体と importStack

PackageError 構造体は、cmd/go ツールがパッケージのロード中に発生したエラーを表現するために使用されます。この構造体には、エラーメッセージ (Err)、エラーが発生した位置 (Pos) などの情報が含まれます。

ImportStack フィールドは、エラーが発生したパッケージに至るまでのインポートパスの最短経路を文字列スライスの形で保持します。例えば、main -> pkgA -> pkgB のようにインポートされている場合、ImportStack["main", "pkgA", "pkgB"] のようになります。インポートサイクルを診断する際には、この ImportStack がサイクルを形成するパスを示すために非常に重要になります。

技術的詳細

このコミットの技術的な核心は、PackageError 構造体の拡張と、そのエラーメッセージ生成ロジックの改善、そしてインポートサイクル検出時の ImportStack の保護です。

  1. PackageError 構造体への isImportCycle フィールド追加: PackageError 構造体に isImportCycle bool という新しいフィールドが追加されました。このフィールドは、発生したエラーがインポートサイクルによるものであるかどうかを示すフラグとして機能します。これにより、エラーの種類に応じて異なる処理を行うことが可能になります。

  2. PackageError.Error() メソッドでのインポートサイクルエラーの特殊な処理: PackageErrorError() メソッドは、エラーメッセージを文字列としてフォーマットする役割を担っています。このコミットでは、p.isImportCycletrue の場合に特別なフォーマットを適用するロジックが追加されました。

    if p.isImportCycle {
        return fmt.Sprintf("%s: %s\npackage %s\n", p.Pos, p.Err, strings.Join(p.ImportStack, "\n\timports "))
    }
    

    この変更により、インポートサイクルエラーの場合、エラーの位置 (p.Pos)、エラーメッセージ (p.Err) に加えて、ImportStack の内容が「imports パッケージ名」という形式で整形されて出力されるようになりました。これにより、どのパッケージがどのパッケージをインポートしているかという循環パスが非常に分かりやすく表示されます。

  3. reusePackage 関数での isImportCycle の設定と、インポートスタックの上書き防止ロジック: reusePackage 関数は、既にロードされたパッケージを再利用する際に、インポートサイクルを検出する主要な場所の一つです。

    • isImportCycle の設定: インポートサイクルが検出された場合(p.imports == nil かつ p.Error == nil の条件)、新しく作成される PackageError オブジェクトの isImportCycle フィールドが true に設定されるようになりました。
      p.Error = &PackageError{
          ImportStack:   stk.copy(),
          Err:           "import cycle not allowed",
          isImportCycle: true, // ここでtrueに設定
      }
      
    • インポートスタックの上書き防止: 以前のコードでは、より短いインポートスタックが見つかった場合に p.Error.ImportStack が無条件に上書きされていました。しかし、インポートサイクルエラーの場合、その ImportStack はサイクルを記述する重要な情報であり、上書きされるべきではありません。このコミットでは、その上書きロジックに条件が追加されました。
      // Don't rewrite the import stack in the error if we have an import cycle.
      // If we do, we'll lose the path that describes the cycle.
      if p.Error != nil && !p.Error.isImportCycle && stk.shorterThan(p.Error.ImportStack) {
          p.Error.ImportStack = stk.copy()
      }
      
      この変更により、p.Error.isImportCycletrue の場合は、ImportStack の上書きが行われなくなりました。これにより、インポートサイクルを正確に診断するための情報が保持されることが保証されます。

これらの変更により、Goのビルドツールはインポートサイクルをより効果的に診断し、開発者にとってより有用なエラーメッセージを提供できるようになりました。

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

src/cmd/go/pkg.go ファイルにおける変更は以下の通りです。

--- a/src/cmd/go/pkg.go
+++ b/src/cmd/go/pkg.go
@@ -129,12 +129,17 @@ func (p *Package) copyBuild(pp *build.Package) {
 
 // A PackageError describes an error loading information about a package.
 type PackageError struct {
-	ImportStack []string // shortest path from package named on command line to this one
-	Pos         string   // position of error
-	Err         string   // the error itself
+	ImportStack   []string // shortest path from package named on command line to this one
+	Pos           string   // position of error
+	Err           string   // the error itself
+	isImportCycle bool     // the error is an import cycle
 }
 
 func (p *PackageError) Error() string {
+\t// Import cycles deserve special treatment.
+\tif p.isImportCycle {
+\t\treturn fmt.Sprintf("%s: %s\npackage %s\n", p.Pos, p.Err, strings.Join(p.ImportStack, "\n\timports "))
+\t}\
  if p.Pos != "" {
  \t\t// Omit import stack.  The full path to the file where the error
  \t\t// is the most important thing.
@@ -271,13 +276,16 @@ func reusePackage(p *Package, stk *importStack) *Package {
  \tif p.imports == nil {\n \t\tif p.Error == nil {\n \t\t\tp.Error = &PackageError{\n-\t\t\t\tImportStack: stk.copy(),\n-\t\t\t\tErr:         "import cycle not allowed",\n+\t\t\t\tImportStack:   stk.copy(),\n+\t\t\t\tErr:           "import cycle not allowed",\n+\t\t\t\tisImportCycle: true,\n \t\t\t}\n \t\t}\n \t\tp.Incomplete = true\n \t}\n-\tif p.Error != nil && stk.shorterThan(p.Error.ImportStack) {\n+\t// Don't rewrite the import stack in the error if we have an import cycle.\n+\t// If we do, we'll lose the path that describes the cycle.\n+\tif p.Error != nil && !p.Error.isImportCycle && stk.shorterThan(p.Error.ImportStack) {\n \t\tp.Error.ImportStack = stk.copy()\n \t}\n \treturn p\n```

## コアとなるコードの解説

1.  **`PackageError` 構造体の変更**:
    *   `isImportCycle bool` フィールドが追加されました。これは、このエラーがインポートサイクルによって引き起こされたものであるかを識別するためのブール値フラグです。これにより、エラー処理ロジックがエラーの種類に基づいて分岐できるようになります。

2.  **`PackageError.Error()` メソッドの変更**:
    *   `if p.isImportCycle` という条件分岐が追加されました。
    *   インポートサイクルエラーの場合、`fmt.Sprintf` を使用して、エラーの位置、エラーメッセージ、そして `strings.Join(p.ImportStack, "\n\timports ")` によって整形されたインポートスタックが結合され、より詳細なエラーメッセージが生成されます。この整形により、`imports パッケージ名` の形式でインポートパスが階層的に表示され、サイクルの経路が視覚的に分かりやすくなります。

3.  **`reusePackage` 関数の変更**:
    *   インポートサイクルが検出され、新しい `PackageError` が作成される際に、`isImportCycle: true` が設定されるようになりました。これにより、このエラーがインポートサイクルであることを明示的にマークします。
    *   既存の `p.Error != nil && stk.shorterThan(p.Error.ImportStack)` 条件に `!p.Error.isImportCycle` が追加されました。これは、「インポートサイクルエラーでない場合にのみ、より短いインポートスタックでエラーのスタックを上書きする」というロジックを意味します。この変更により、インポートサイクルを記述する重要な `ImportStack` 情報が、後続の処理で誤って上書きされて失われることを防ぎます。

これらの変更は、Goのビルドツールがインポートサイクルをより正確に検出し、その原因を開発者に明確に伝えるための基盤を強化しています。

## 関連リンク

*   Go issue #4292: [https://code.google.com/p/go/issues/detail?id=4292](https://code.google.com/p/go/issues/detail?id=4292) (現在はGitHubに移行済み)
*   Gerrit Change-Id: [https://golang.org/cl/13100043](https://golang.org/cl/13100043)

## 参考にした情報源リンク

*   Go言語の公式ドキュメント (パッケージとモジュールに関する情報)
*   Go言語のソースコード (`src/cmd/go/pkg.go` の変更前後の比較)
*   Go issue #4292 の議論内容
*   Go言語のインポートサイクルに関する一般的な解説記事