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

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

このコミットは、Go言語のcmd/apiツールにおけるパッケージインポート処理の重複作業を排除し、パフォーマンスを大幅に改善することを目的としています。具体的には、パッケージのインポート結果をキャッシュすることで、APIチェックにかかる時間を短縮し、同時にビルドタグに関する混乱を解消しています。

コミット

commit b78410bda13cc10c1e59dfdcc935b3155450b44e
Author: Russ Cox <rsc@golang.org>
Date:   Fri Aug 9 18:44:00 2013 -0400

    cmd/api: eliminate duplicate package import work
    
    On my Mac, cuts the API checks from 15 seconds to 6 seconds.
    Also clean up some tag confusion: go run list-of-files ignores tags.
    
    R=bradfitz, gri
    CC=golang-dev
    https://golang.org/cl/12699048

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

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

元コミット内容

cmd/apiツールにおいて、重複するパッケージインポート作業を排除する。 これにより、Mac上でのAPIチェック時間が15秒から6秒に短縮された。 また、タグに関する混乱(go runがファイルリストを無視するタグ)も解消された。

変更の背景

Go言語のcmd/apiツールは、GoパッケージのエクスポートされたAPIを計算するために使用されます。このツールは、Goの標準ライブラリやその他のパッケージのAPIが、Goのバージョンアップによって意図せず変更されていないかを確認するために、GoプロジェクトのCI/CDパイプラインや開発プロセスにおいて重要な役割を果たします。

このコミットが導入される以前は、cmd/apiツールがパッケージをインポートする際に、同じパッケージであっても異なるビルドコンテキスト(例えば、異なるOSやアーキテクチャ、あるいは特定のビルドタグが有効になっているかどうか)で複数回インポートされる場合、その都度、パッケージの解析や型チェックといった時間のかかる処理が重複して実行されていました。これは特に、多数のパッケージを扱う場合や、異なるビルドコンテキストでAPIをチェックする必要がある場合に、ツールの実行時間を著しく長くしていました。コミットメッセージにある「Mac上でAPIチェックが15秒から6秒に短縮された」という記述は、この重複作業がパフォーマンスに与えていた影響の大きさを物語っています。

また、このコミットは「タグに関する混乱」も解消しています。具体的には、go runコマンドが特定のビルドタグ(例: from_src_run)をどのように扱うか、あるいは無視するかについての挙動が不明瞭であったり、意図しない結果を引き起こしたりする可能性がありました。go runは通常、指定されたGoソースファイルをコンパイルして実行しますが、ビルドタグはコンパイル時にどのファイルを含めるかを制御します。このコミットでは、cmd/api/run.goのビルドタグを+build ignoreに変更し、src/run.bashおよびsrc/run.batから--tags=from_src_runオプションを削除することで、cmd/apiツールの実行方法を簡素化し、ビルドタグの適用に関する混乱を解消しています。これにより、cmd/apiツールがより予測可能で効率的に動作するようになりました。

前提知識の解説

このコミットの理解には、以下のGo言語の概念とツールに関する知識が不可欠です。

  • go runコマンド: go runは、Goのソースファイルをコンパイルし、その結果生成された実行可能ファイルを一時的に実行するためのコマンドです。開発中に単一のGoプログラムを素早くテストする際によく使用されます。通常、コンパイルされたバイナリは一時ディレクトリに保存され、実行後に削除されます。

  • cmd/apiツール: Goのソースコードリポジトリに含まれる内部ツールの一つで、GoパッケージのエクスポートされたAPI(公開されている型、関数、変数など)を抽出・比較するために使用されます。Go言語の互換性保証(Go 1互換性ポリシーなど)を維持するために、Goのリリースプロセスにおいて重要な役割を果たします。このツールは、go/buildパッケージとgo/typesパッケージを利用して、Goのソースコードを解析し、API情報を抽出します。

  • go/buildパッケージ: Goの標準ライブラリの一部で、Goパッケージのビルドプロセスに関する情報を提供します。これには、ソースファイルの検索、パッケージの依存関係の解決、ビルドタグの処理などが含まれます。build.Context構造体は、特定のビルド環境(OS、アーキテクチャ、ビルドタグなど)を表現し、パッケージのインポート時に使用されます。

  • go/typesパッケージ: Goの標準ライブラリの一部で、Goプログラムの型チェックを行うための機能を提供します。このパッケージは、Goのソースコードを抽象構文木(AST)として解析し、そのASTに対して型情報を付与し、型エラーを検出します。cmd/apiツールは、このパッケージを使用して、パッケージ内のエクスポートされたシンボルの型情報を正確に把握します。

  • Goのビルドタグ (Build Tags): Goのソースファイルには、// +build tagのようなコメント行を追加することで、ビルドタグを指定できます。これにより、特定のタグが有効な場合にのみ、そのソースファイルがコンパイルに含まれるように制御できます。例えば、// +build linuxはLinuxシステムでのみコンパイルされるファイルを示します。ビルドタグは、プラットフォーム固有のコードや、特定の機能の有効/無効を切り替えるために使用されます。

  • Goのパッケージインポートプロセス: Goプログラムがimportステートメントを使用して他のパッケージを参照すると、Goツールチェーンは以下の手順でパッケージを解決し、ロードします。

    1. パッケージの検索: GOPATHGOROOTなどの環境変数に基づいて、指定されたパッケージ名のソースコードディレクトリを検索します。
    2. ビルドコンテキストの適用: 現在のOS、アーキテクチャ、および有効なビルドタグに基づいて、どのソースファイルがコンパイルに含まれるべきかを決定します。
    3. ソースファイルの解析: 選択されたソースファイルを解析し、抽象構文木(AST)を構築します。
    4. 型チェック: go/typesパッケージなどを使用して、ASTに対して型チェックを実行し、パッケージのエクスポートされたAPIを含む型情報を生成します。 このプロセスは、特に大規模なプロジェクトや、多くの依存関係を持つパッケージの場合、時間とリソースを消費します。

技術的詳細

このコミットの主要な技術的変更点は、cmd/apiツールにおけるパッケージインポートのキャッシュメカニズムの導入と、ビルドタグの扱いに関する改善です。

1. パッケージインポートのキャッシュ

src/cmd/api/goapi.goに、以下の2つのグローバルマップが導入されました。

  • pkgCache = map[string]*types.Package{}: これは、既にインポートされ、型チェックが完了したtypes.Packageオブジェクトをキャッシュするためのマップです。キーは、パッケージのディレクトリと、そのパッケージのインポートに関連するビルドタグの組み合わせから生成される一意の文字列(tagKey関数によって生成)です。これにより、同じビルドコンテキストで同じパッケージが再度インポートされる際に、高コストな解析と型チェックのプロセスをスキップし、キャッシュされた結果を再利用できます。

  • pkgTags = map[string][]string{}: これは、各パッケージディレクトリに関連するすべてのビルドタグのリストを保存するためのマップです。キーはパッケージのディレクトリパスです。go/buildパッケージのPackage.AllTagsフィールドから取得されるこのタグリストは、tagKeyを生成する際に、どのタグが現在のビルドコンテキストで「関連する」かを判断するために使用されます。

tagKey関数の導入

tagKey関数は、キャッシュのキーを生成する中心的な役割を担います。この関数は、パッケージのディレクトリパス、現在のビルドコンテキスト(build.Context)、およびそのパッケージに関連するすべてのビルドタグのリストを受け取ります。 関数内部では、現在のビルドコンテキストで有効なタグ(GOOS, GOARCH, cgo, BuildTags)をセットとして構築し、pkgTagsから取得したパッケージ固有のタグリストの中から、この有効なタグセットに含まれるタグのみを抽出します。これらのタグはソートされた順序でディレクトリパスにカンマ区切りで連結され、一意のキャッシュキーが生成されます。これにより、同じパッケージであっても、異なるビルドコンテキスト(例えば、linux,amd64windows,amd64)では異なるキャッシュキーが生成され、適切なパッケージバージョンがキャッシュから取得されるようになります。

Walker.Importメソッドの変更

Walker.Importメソッドは、cmd/apiツールがGoパッケージをインポートする際の主要なエントリポイントです。このメソッドに以下のキャッシュロジックが追加されました。

  1. キャッシュのチェック: パッケージのインポートが要求された際、まずpkgTagsマップをチェックして、そのディレクトリが以前にインポートされたことがあるかを確認します。もしあれば、tagKey関数を使用してキャッシュキーを生成し、pkgCacheマップにそのキーが存在するかどうかを調べます。存在し、かつnilでない(つまり、インポート処理中ではない)場合は、キャッシュされたtypes.Packageオブジェクトが即座に返され、重複する作業が回避されます。

  2. build.Context.ImportDirの呼び出し: キャッシュミスが発生した場合、または初回インポートの場合にのみ、context.ImportDirが呼び出され、実際のパッケージ情報がファイルシステムから読み込まれます。

  3. pkgTagsの更新: パッケージディレクトリが初めてインポートされる場合、info.AllTagsbuild.Packageから取得される、そのパッケージに関連するすべてのビルドタグのリスト)がpkgTagsマップに保存されます。これにより、将来的に同じディレクトリがインポートされる際に、tagKeyを正確に生成できるようになります。

  4. pkgCacheへの保存: パッケージの型チェックが正常に完了した後、生成されたtypes.Packageオブジェクトは、tagKeyによって生成されたキーを使用してpkgCacheに保存されます。これにより、後続のインポート要求で再利用できるようになります。

2. ビルドタグのクリーンアップ

このコミットでは、src/cmd/api/run.gosrc/run.bashsrc/run.batにおけるビルドタグの扱いも変更されました。

  • src/cmd/api/run.goの変更: ファイルの先頭にあった// +build from_src_runというビルドタグが// +build ignoreに変更されました。 +build ignoreは、そのファイルがGoのビルドプロセスから完全に無視されることを意味します。これは、src/cmd/api/run.gogo runコマンドによって直接実行されることを意図しており、通常のgo buildプロセスでコンパイルされるべきではないことを明確に示しています。

  • src/run.bashsrc/run.batの変更: これらのスクリプトは、Goのテストスイートの一部としてcmd/apiツールを実行するために使用されます。以前は、go run --tags=from_src_run $GOROOT/src/cmd/api/run.goのように、--tags=from_src_runオプションを明示的に指定してgo runを呼び出していました。このコミットでは、このオプションが削除され、単にgo run $GOROOT/src/cmd/api/run.goと呼び出すようになりました。 この変更は、src/cmd/api/run.go+build ignoreになったことと合わせて、cmd/apiツールの実行方法を簡素化し、ビルドタグの適用に関する混乱を解消することを目的としています。go runは、指定されたファイルを直接実行するため、そのファイルに+build ignoreがあっても実行できます。これにより、cmd/apiツールが特定のビルドタグに依存することなく、より直接的に実行されるようになりました。

これらの変更により、cmd/apiツールのパフォーマンスが向上し、ビルドシステムにおけるタグの扱いがより明確になりました。

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

src/cmd/api/goapi.go

--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -1,9 +1,9 @@
-// +build api_tool
-
 // Copyright 2011 The Go Authors.  All rights reserved.
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// +build api_tool
+
 // Binary api computes the exported API of a set of Go packages.
 package main
 
@@ -387,6 +387,38 @@ func contains(list []string, s string) bool {
 	return false
 }
 
+var (
+	pkgCache = map[string]*types.Package{} // map tagKey to package
+	pkgTags  = map[string][]string{}       // map import dir to list of relevant tags
+)
+
+// tagKey returns the tag-based key to use in the pkgCache.
+// It is a comma-separated string; the first part is dir, the rest tags.
+// The satisfied tags are derived from context but only those that
+// matter (the ones listed in the tags argument) are used.
+// The tags list, which came from go/build's Package.AllTags,
+// is known to be sorted.
+func tagKey(dir string, context *build.Context, tags []string) string {
+	ctags := map[string]bool{
+		context.GOOS:   true,
+		context.GOARCH: true,
+	}
+	if context.CgoEnabled {
+		ctags["cgo"] = true
+	}
+	for _, tag := range context.BuildTags {
+		ctags[tag] = true
+	}
+	// TODO: ReleaseTags (need to load default)
+	key := dir
+	for _, tag := range tags {
+		if ctags[tag] {
+			key += "," + tag
+		}
+	}
+	return key
+}
+
 // Importing is a sentinel taking the place in Walker.imported
 // for a package that is in the process of being imported.\
 var importing types.Package
@@ -411,6 +443,19 @@ func (w *Walker) Import(name string) (pkg *types.Package) {
 	if context == nil {
 		context = &build.Default
 	}
+
+	// Look in cache.
+	// If we've already done an import with the same set
+	// of relevant tags, reuse the result.
+	var key string
+	if tags, ok := pkgTags[dir]; ok {
+		key = tagKey(dir, context, tags)
+		if pkg := pkgCache[key]; pkg != nil {
+			w.imported[name] = pkg
+			return pkg
+		}
+	}
+
 	info, err := context.ImportDir(dir, 0)
 	if err != nil {
 		if _, nogo := err.(*build.NoGoError); nogo {
@@ -418,6 +463,13 @@ func (w *Walker) Import(name string) (pkg *types.Package) {
 		}
 		log.Fatalf("pkg %q, dir %q: ScanDir: %v", name, dir, err)
 	}\
+
+	// Save tags list first time we see a directory.
+	if _, ok := pkgTags[dir]; !ok {
+		pkgTags[dir] = info.AllTags
+		key = tagKey(dir, context, info.AllTags)
+	}
+
 	filenames := append(append([]string{}, info.GoFiles...), info.CgoFiles...)\
 
 	// Certain files only exist when building for the specified context.\
@@ -463,6 +515,8 @@ func (w *Walker) Import(name string) (pkg *types.Package) {
 		log.Fatalf("error typechecking package %s: %s (%s)", name, err, ctxt)
 	}
 
+	pkgCache[key] = pkg
+
 	w.imported[name] = pkg
 	return
 }

src/cmd/api/run.go

--- a/src/cmd/api/run.go
+++ b/src/cmd/api/run.go
@@ -1,9 +1,9 @@
-// +build from_src_run
-
 // Copyright 2013 The Go Authors.  All rights reserved.
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// +build ignore
+
 // The run program is invoked via "go run" from src/run.bash or
 // src/run.bat conditionally builds and runs the cmd/api tool.
 //

src/run.bash

--- a/src/run.bash
+++ b/src/run.bash
@@ -182,7 +182,7 @@ time go run run.go || exit 1
 
 echo
 echo '# Checking API compatibility.'
-go run --tags=from_src_run $GOROOT/src/cmd/api/run.go
+time go run $GOROOT/src/cmd/api/run.go
 
 echo
 echo ALL TESTS PASSED

src/run.bat

--- a/src/run.bat
+++ b/src/run.bat
@@ -121,7 +121,7 @@ set GOMAXPROCS=%OLDGOMAXPROCS%\n set OLDGOMAXPROCS=\n \n echo # Checking API compatibility.\n-go run --tags=from_src_run "%GOROOT%\\src\\cmd\\api\\run.go"\n+go run "%GOROOT%\\src\\cmd\\api\\run.go"\n if errorlevel 1 goto fail\n echo.\n \n```

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

### `src/cmd/api/goapi.go`の変更

1.  **グローバル変数の追加**:
    `pkgCache`と`pkgTags`という2つのグローバルマップが追加されました。
    *   `pkgCache`: `tagKey`(文字列)をキーとし、`*types.Package`(型チェック済みのパッケージ情報)を値とするマップです。これにより、同じビルドコンテキストでインポートされたパッケージの情報を再利用できます。
    *   `pkgTags`: パッケージのディレクトリパス(文字列)をキーとし、そのパッケージに関連するすべてのビルドタグのリスト(`[]string`)を値とするマップです。これは、`tagKey`を生成する際に、そのパッケージにどのタグが関連しているかを効率的に取得するために使用されます。

2.  **`tagKey`関数の追加**:
    この関数は、パッケージのディレクトリと現在のビルドコンテキスト(OS、アーキテクチャ、Cgoの有効/無効、カスタムビルドタグ)に基づいて、キャッシュキーを生成します。
    *   `ctags`マップは、現在のビルドコンテキストで有効なタグを効率的にルックアップするために使用されます。
    *   引数`tags`は、`go/build`パッケージから取得した、そのパッケージに関連する可能性のあるすべてのタグのリストです。
    *   `key`文字列は、まずディレクトリパスで初期化され、その後、`tags`リストの中から`ctags`マップに存在する(つまり、現在のビルドコンテキストで有効な)タグがカンマ区切りで追加されます。これにより、同じパッケージでもビルドコンテキストが異なれば異なるキーが生成され、適切なキャッシュエントリが選択されるようになります。

3.  **`Walker.Import`メソッドの変更**:
    このメソッドは、パッケージをインポートする際の主要なロジックを含んでいます。
    *   **キャッシュの利用**: メソッドの冒頭で、`pkgTags`と`pkgCache`を利用して、既に同じビルドコンテキストでこのパッケージがインポートされていないかを確認します。
        *   `if tags, ok := pkgTags[dir]; ok`: まず、このディレクトリが以前にインポートされたことがあり、関連タグが`pkgTags`に保存されているかを確認します。
        *   `key = tagKey(dir, context, tags)`: 関連タグが見つかった場合、`tagKey`関数を使ってキャッシュキーを生成します。
        *   `if pkg := pkgCache[key]; pkg != nil`: 生成されたキーで`pkgCache`をチェックし、キャッシュされたパッケージが存在すれば、それを`w.imported`に設定してすぐに返します。これにより、重複するインポート作業が回避されます。
    *   **`pkgTags`の更新**: `context.ImportDir`が呼び出され、パッケージ情報が実際に読み込まれた後、そのディレクトリが`pkgTags`にまだ存在しない場合(つまり、そのディレクトリが初めてインポートされる場合)、`info.AllTags`(`build.Package`から取得される、そのパッケージに関連するすべてのタグ)が`pkgTags`に保存されます。これにより、将来のインポートで`tagKey`を正確に生成できるようになります。
    *   **`pkgCache`への保存**: パッケージの型チェックが完了し、`pkg`オブジェクトが生成された後、`pkgCache[key] = pkg`という行が追加され、生成されたパッケージがキャッシュに保存されます。

### `src/cmd/api/run.go`の変更

*   ファイルの先頭のビルドタグが`// +build from_src_run`から`// +build ignore`に変更されました。`+build ignore`は、`go build`コマンドがこのファイルを無視することを意味します。これは、このファイルが`go run`によって直接実行されることを意図しているためです。

### `src/run.bash`と`src/run.bat`の変更

*   `go run`コマンドの呼び出しから`--tags=from_src_run`オプションが削除されました。これは、`src/cmd/api/run.go`が`+build ignore`になったため、特定のビルドタグを指定する必要がなくなったためです。これにより、スクリプトが簡素化され、ビルドタグの扱いに関する混乱が解消されました。

これらの変更により、`cmd/api`ツールは、同じパッケージを異なるビルドコンテキストで複数回処理する際に、高コストなインポートと型チェックの作業を大幅に削減できるようになり、実行パフォーマンスが劇的に向上しました。また、ビルドタグの扱いもより明確で予測可能なものになりました。

## 関連リンク

*   [https://golang.org/cl/12699048](https://golang.org/cl/12699048)

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

*   Go言語の公式ドキュメント(`go/build`パッケージ、`go/types`パッケージ、`go run`コマンドに関する情報)
*   Go言語のビルドタグに関する情報
*   Go言語のパッケージインポートメカニズムに関する一般的な知識
*   `cmd/api`ツールの目的と機能に関する情報
*   Goのソースコード(特に`src/cmd/api`ディレクトリ)
*   Goのコミット履歴と関連する議論