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

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

コミット

このコミットは、Go言語のパッケージ管理ツールである goinstall コマンドの動作を改善するものです。具体的には、古いGoogle Codeのインポートパス(例: project.googlecode.com/...)が検出された際に、goinstall -fix オプションの使用を提案するメッセージの表示ロジックを洗練しています。この変更により、問題のあるインポートパスがコマンドラインで直接指定されたものではなく、他のパッケージの依存関係として現れた場合にのみ、-fix の提案が行われるようになります。これにより、ユーザーへのガイダンスがより適切かつ的確になり、不必要な提案を避けることができます。

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

https://github.com/golang/go/commit/76a078332173ab49e0a9d3fad4854960ee0b1c50

元コミット内容

goinstall: only suggest -fix for bad imports when appropriate

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5495073

変更の背景

Go言語の初期の段階では、多くのオープンソースプロジェクトがGoogle Code上でホストされていました。Goのパッケージインポートパスは、通常、そのパッケージのソースコードがホストされているリポジトリのURLに似た形式を取ります。例えば、projectname.googlecode.com/svn/path/to/package のような形式が使われていました。しかし、Goのツールチェーンは後に、より統一された code.google.com/p/projectname/path/to/package という形式を推奨し、この形式への移行が進められました。

goinstall は、Goパッケージをダウンロードし、ビルドしてインストールするための初期のコマンドラインツールでした(現在の go get コマンドの前身)。この goinstall は、古い googlecode.com 形式のインポートパスに遭遇した場合、新しい code.google.com/p/ 形式に修正する必要があることをユーザーに通知し、さらに goinstall -fix コマンドを実行することで自動的に修正できることを提案していました。

しかし、この -fix の提案は、常にユーザーにとって最適なタイミングで表示されるわけではありませんでした。特に、ユーザーがコマンドラインで直接古い形式のインポートパスを指定した場合、そのパスはユーザーが意図的に指定したものである可能性があり、その場で -fix の実行を促すメッセージは冗長であったり、ユーザーを混乱させたりする可能性がありました。

このコミットの目的は、このようなユーザー体験の課題を解決することにあります。goinstall が、古いインポートパスの問題が、ユーザーが直接指定したパッケージではなく、そのパッケージが依存している別のパッケージ(つまり、間接的な依存関係)で発生した場合にのみ、-fix の提案を行うようにロジックを調整することで、より賢く、より適切なタイミングでユーザーにガイダンスを提供することを目指しています。これにより、エラーメッセージの関連性が高まり、ユーザーは本当に修正が必要な場合にのみ、その提案を受け取ることができるようになります。

前提知識の解説

goinstall コマンド

goinstall は、Go言語の初期に開発者がパッケージを管理するために使用していたコマンドラインツールです。その主な機能は、指定されたインポートパスに基づいて、リモートのバージョン管理システム(Mercurial, Git, Subversionなど)からGoパッケージのソースコードをダウンロードし、コンパイルして、GOPATH 環境変数で指定されたワークスペースにインストールすることでした。このコマンドは、Goエコシステムが成熟するにつれて、より高機能で統合された go get コマンドに置き換えられ、現在ではほとんど使用されていません。しかし、このコミットが作成された2011年当時は、Goパッケージ管理の主要なツールの一つでした。

Goのインポートパスの仕組み

Go言語では、パッケージは一意のインポートパスによって識別されます。このパスは、通常、パッケージのソースコードがホストされているリポジトリのURLに直接対応しています。例えば、github.com/user/repo/package のような形式です。Goのツールチェーン(goinstallgo get)は、このインポートパスを解析し、対応するリポジトリからソースコードを取得します。この仕組みにより、Goのパッケージは分散された環境で簡単に共有・再利用できるようになっています。

Google Code と Goプロジェクトのホスティングの歴史

Google Codeは、かつてGoogleが提供していた無料のオープンソースプロジェクトホスティングサービスです。Go言語が誕生した初期の頃、多くのGoプロジェクトがこのGoogle Code上でホストされていました。Google CodeのリポジトリURLは、projectname.googlecode.com/svnprojectname.googlecode.com/git のような形式でした。しかし、Go言語のツールチェーンは、より簡潔で標準的な code.google.com/p/projectname という形式を推奨し、多くのプロジェクトがこの新しい形式に移行しました。この移行期間中、古い形式のインポートパスが既存のコードベースや依存関係の中に残存していることがあり、これが goinstall が対処すべき問題の一つとなっていました。

-fix オプションと gofix ツール

Go言語は、言語仕様や標準ライブラリの進化に伴い、既存のコードを新しいAPIや慣習に自動的に適合させるためのツールを提供しています。その代表的なものが gofix です。gofix は、Goのソースコードを解析し、非推奨となった構文やAPIの使用箇所を自動的に修正する機能を持っています。goinstall コマンドの -fix オプションは、この gofix ツールを内部的に呼び出すものでした。つまり、goinstall -fix を実行すると、ダウンロードされたパッケージのコードに対して gofix が適用され、古いGoogle Codeのインポートパスのような既知の互換性の問題を自動的に修正することが可能でした。

技術的詳細

このコミットの技術的な核心は、Goのエラーハンドリングにおける「エラーの型」を利用した条件分岐の洗練にあります。これにより、goinstall は特定のエラー状況をより正確に識別し、それに応じた適切なユーザーフィードバックを提供できるようになりました。

  1. errOldGoogleRepo 型の導入: src/cmd/goinstall/download.go ファイルに、errOldGoogleRepo という新しいカスタムエラー型が定義されました。

    type errOldGoogleRepo struct {
        fixedPath string
    }
    
    func (e *errOldGoogleRepo) Error() string {
        return fmt.Sprintf("unsupported import path; should be %q", e.fixedPath)
    }
    

    この構造体は、古いGoogle Codeのインポートパスが検出されたことを示すために使用されます。fixedPath フィールドには、修正後の正しいインポートパス(例: code.google.com/p/projectname)が格納されます。また、Error() メソッドを実装しているため、Goの組み込み error インターフェースを満たします。これにより、このカスタムエラーは通常の error 型として扱われつつも、その具体的な型情報を保持し、後続の処理で型アサーションによって識別することが可能になります。

  2. download 関数でのエラー生成の変更: download 関数は、指定されたインポートパスに基づいてパッケージをダウンロードする役割を担っています。この関数内で、oldGoogleRepo という正規表現がインポートパスにマッチした場合(つまり、古いGoogle Codeの形式が検出された場合)、以前は fmt.Errorf を使用して、修正後のパスと -fix オプションの実行を促すメッセージを含む汎用的なエラー文字列を生成していました。 このコミットにより、その汎用的なエラーメッセージの生成が削除され、代わりに新しく定義された errOldGoogleRepo 型のインスタンスが返されるようになりました。

    // 変更前: 汎用的なエラーメッセージを生成
    // err = fmt.Errorf(
    //     "unsupported import path; should be %q\n" +
    //     "\tRun goinstall with -fix to gofix the code.",
    //     fixedPath,
    // )
    // 変更後: 特定のエラー型を返す
    err = &errOldGoogleRepo{fixedPath}
    

    この変更により、エラーが発生した時点では -fix の提案はエラーメッセージに含まれず、エラーの「種類」と「修正後のパス」という情報のみが、型情報として伝達されることになります。

  3. install 関数でのエラーハンドリングの改善: src/cmd/goinstall/main.goinstall 関数は、パッケージのインストールプロセス全体をオーケストレーションします。この関数内で download 関数が呼び出され、その結果として返されるエラーが処理されます。 変更されたコードブロックは以下の通りです。

    if err != nil {
        // only suggest -fix if the bad import was not on the command line
        if e, ok := err.(*errOldGoogleRepo); ok && parent != "" {
            err = fmt.Errorf("%v\nRun goinstall with -fix to gofix the code.", e)
        }
        return &DownloadError{pkg, tree.Goroot, err}
    }
    

    ここで重要なのは、if e, ok := err.(*errOldGoogleRepo); ok というGoの型アサーション構文です。これは、download 関数から返された err*errOldGoogleRepo 型であるかどうかをチェックします。もしそうであれば、そのエラーの具体的な値が変数 e に代入され、oktrue となります。 さらに、parent != "" という条件が追加されています。parent 引数は、現在処理中のパッケージが、別のパッケージの依存関係としてダウンロードされている場合に、その親パッケージのインポートパスを保持します。もし parent が空文字列 ("") であれば、そのパッケージはユーザーがコマンドラインで直接指定したものであることを意味します。 したがって、この if 文は、以下の両方の条件が満たされた場合にのみ真となります。

    • ダウンロード中に古いGoogle Codeのインポートパスに関するエラーが発生し、それが errOldGoogleRepo 型である。
    • そのエラーが発生したパッケージが、コマンドラインで直接指定されたものではなく、他のパッケージの依存関係としてダウンロードされている。

    これらの条件が満たされた場合にのみ、元の errOldGoogleRepo のエラーメッセージ(e.Error() が返す文字列、例: unsupported import path; should be "code.google.com/p/...")に加えて、「Run goinstall with -fix to gofix the code.」という -fix の提案が追加された新しいエラーが fmt.Errorf によって生成され、err に再代入されます。

この一連の変更により、goinstall は、ユーザーが直接指定したインポートパスが古い形式であっても、すぐに冗長な -fix の提案を行うことを避け、依存関係の解決中に発生した古いインポートパスの問題に対してのみ、より適切なタイミングで、かつ具体的な修正方法を促すメッセージを提供するようになりました。これは、エラーメッセージの関連性を高め、ユーザー体験を向上させるための、Goのエラーハンドリングのベストプラクティスに沿った洗練された改善と言えます。

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

src/cmd/goinstall/download.go

--- a/src/cmd/goinstall/download.go
+++ b/src/cmd/goinstall/download.go
@@ -367,6 +367,14 @@ func (v *vcs) findURL(root string) (string, error) {
 
 var oldGoogleRepo = regexp.MustCompile(`^([a-z0-9\\-]+)\\.googlecode\\.com/(svn|git|hg)(/[a-z0-9A-Z_.\\-/]+)?$`)
 
+type errOldGoogleRepo struct {
+	fixedPath string
+}
+
+func (e *errOldGoogleRepo) Error() string {
+	return fmt.Sprintf("unsupported import path; should be %q", e.fixedPath)
+}
+
 // download checks out or updates the specified package from the remote server.
 func download(importPath, srcDir string) (public bool, err error) {
 	if strings.Contains(importPath, "..") {
@@ -376,11 +384,7 @@ func download(importPath, srcDir string) (public bool, err error) {
 
 	if m := oldGoogleRepo.FindStringSubmatch(importPath); m != nil {
 		fixedPath := "code.google.com/p/" + m[1] + m[3]
-		err = fmt.Errorf(
-			"unsupported import path; should be %q\\n"+
-			"\tRun goinstall with -fix to gofix the code.",
-			fixedPath,
-		)
+		err = &errOldGoogleRepo{fixedPath}
 		return
 	}
 

src/cmd/goinstall/main.go

--- a/src/cmd/goinstall/main.go
+++ b/src/cmd/goinstall/main.go
@@ -249,6 +249,10 @@ func install(pkg, parent string) error {
 			printf("%s: download\\n", pkg)
 			public, err = download(pkg, tree.SrcDir())
 			if err != nil {
+				// only suggest -fix if the bad import was not on the command line
+				if e, ok := err.(*errOldGoogleRepo); ok && parent != "" {
+					err = fmt.Errorf("%v\\nRun goinstall with -fix to gofix the code.", e)
+				}
 				return &DownloadError{pkg, tree.Goroot, err}
 			}
 		} else {

コアとなるコードの解説

src/cmd/goinstall/download.go の変更点詳細

  1. errOldGoogleRepo 型の追加: この新しいGoの構造体型は、Go言語におけるカスタムエラーの典型的な実装パターンを示しています。fixedPath という文字列フィールドを持ち、これは古いGoogle Codeのインポートパスが検出された際に、そのパッケージの正しい新しいインポートパスを保持するために使用されます。 func (e *errOldGoogleRepo) Error() string メソッドの実装により、この構造体はGoの組み込み error インターフェースを満たします。このメソッドは、エラーが発生した際に表示される基本的なメッセージ(例: "unsupported import path; should be \"code.google.com/p/project\"")を生成します。この段階では、まだ -fix オプションに関する具体的な提案は含まれていません。これは、エラーの「種類」と「修正後のパス」という純粋な情報のみをカプセル化することを目的としています。

  2. download 関数内のエラー生成ロジックの変更: download 関数は、Goパッケージのソースコードをリモートリポジトリから取得する主要なロジックを含んでいます。この関数内で、oldGoogleRepo という正規表現(project.googlecode.com/... のような古い形式のインポートパスを検出するためのもの)が、現在処理中の importPath にマッチした場合の処理が変更されました。 変更前は、fmt.Errorf を直接使用して、修正後のパスと -fix オプションの実行を促すメッセージを組み合わせた、単一の文字列エラーを生成していました。この方法はシンプルですが、エラーの具体的な「種類」をプログラム的に識別することが困難でした。 変更後は、err = &errOldGoogleRepo{fixedPath} という行により、errOldGoogleRepo 型の新しいインスタンスが作成され、その fixedPath フィールドに正しいインポートパスが設定されます。そして、このカスタムエラーインスタンスが download 関数の戻り値として返されます。これにより、エラーハンドリングを行う呼び出し元(この場合は install 関数)は、返されたエラーが単なる文字列エラーではなく、特定の意味を持つ errOldGoogleRepo 型のエラーであることを型アサーションによって識別できるようになります。

src/cmd/goinstall/main.go の変更点詳細

  1. install 関数内のエラーハンドリングロジックの改善: install 関数は、goinstall コマンドの主要なインストールフローを管理し、その中で download 関数を呼び出します。download 関数からエラーが返された場合、この install 関数がそのエラーを処理します。 変更されたエラー処理ブロックは、Goの強力な型アサーション機能と条件分岐を組み合わせています。
    if err != nil { // download関数からエラーが返された場合
        // only suggest -fix if the bad import was not on the command line
        // エラーがerrOldGoogleRepo型であり、かつ、そのパッケージがコマンドラインで直接指定されたものではない場合のみ-fixを提案
        if e, ok := err.(*errOldGoogleRepo); ok && parent != "" {
            // errOldGoogleRepoのエラーメッセージに、-fixの提案を追加して新しいエラーを生成
            err = fmt.Errorf("%v\nRun goinstall with -fix to gofix the code.", e)
        }
        // 最終的なエラーをDownloadErrorとしてラップして返す
        return &DownloadError{pkg, tree.Goroot, err}
    }
    
    • if e, ok := err.(*errOldGoogleRepo); ok: これはGoの型アサーションです。download 関数から返された err*errOldGoogleRepo 型であるかどうかをチェックします。もしそうであれば、oktrue となり、err の値は e という *errOldGoogleRepo 型の変数に代入されます。これにより、エラーの具体的な内容(fixedPath など)にアクセスできるようになります。
    • parent != "": この条件は、現在処理中のパッケージが、コマンドラインで直接指定されたものではなく、他のパッケージの依存関係としてダウンロードされているかどうかを判断します。parent 変数は、親パッケージのインポートパスを保持しており、もし parent が空文字列 ("") であれば、それは最上位の(コマンドラインで指定された)パッケージであることを意味します。
    • if 文の条件が両方とも真(つまり、古いGoogle Codeのインポートパスに関するエラーが依存関係で発生した場合)であれば、fmt.Errorf("%v\nRun goinstall with -fix to gofix the code.", e) という行が実行されます。ここで、%v フォーマット動詞は e*errOldGoogleRepo 型)の Error() メソッドを呼び出し、その結果(例: "unsupported import path; should be \"code.google.com/p/project\"")を取得します。その後に、具体的な -fix オプションの実行を促すメッセージが追加され、この新しいエラーが err 変数に再代入されます。

このロジックにより、goinstall は、ユーザーが直接指定したインポートパスに対しては、単に「このパスはサポートされていません、修正後のパスはこれです」というメッセージのみを表示し、冗長な -fix の提案は行いません。しかし、依存関係の解決中に古いインポートパスが検出された場合には、より詳細なエラーメッセージと、自動修正のための -fix オプションの実行を促すガイダンスを提供します。これは、ユーザーが直面している問題の文脈に応じて、最も役立つ情報を提供するという点で、ユーザーインターフェースとエラー報告の品質を大幅に向上させる変更です。

関連リンク

参考にした情報源リンク