[インデックス 14318] ファイルの概要
このコミットは、Go言語のビルドツール (cmd/go
) における、競合検出器 (race detector) を有効にした際のパス解決の問題を修正するものです。具体的には、GOPATH
内のパッケージが別の GOPATH
内のパッケージをインポートしている場合に、-race
フラグを付けてビルドすると失敗するという問題を解決します。
コミット
commit fb9706d3bed364276c075081fbab820719fc5965
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Nov 6 20:11:49 2012 +0400
cmd/go: use correct paths with race detector
Currently the build fails with -race if a package in GOPATH
imports another package in GOPATH.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6811083
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fb9706d3bed364276c075081fbab820719fc5965
元コミット内容
cmd/go
: 競合検出器で正しいパスを使用する
現在、GOPATH
内のパッケージが別の GOPATH
内のパッケージをインポートしている場合、-race
を指定するとビルドが失敗する。
変更の背景
Go言語のビルドシステムにおいて、競合検出器 (-race
フラグ) を有効にした際に、特定の条件下でビルドが失敗するという問題が発生していました。この問題は、特にユーザーが GOPATH
環境変数で指定したワークスペース内に複数のパッケージが存在し、それらのパッケージが互いに依存している場合に顕著でした。
競合検出器は、プログラムの実行中に発生する可能性のあるデータ競合(複数のゴルーチンが同時に共有データにアクセスし、少なくとも1つが書き込み操作を行うことで、実行結果が非決定論的になる状態)を検出するための強力なツールです。この機能を実現するために、Goコンパイラは通常、コードに特別なインストゥルメンテーション(計測コード)を挿入します。このインストゥルメンテーションは、通常のビルドとは異なるコンパイル済みバイナリや中間ファイルを生成する可能性があります。
問題の根本原因は、cmd/go
ツールが、競合検出器を有効にしたビルドと通常のビルドで、生成される中間ファイルやキャッシュされるパッケージのパスを適切に区別していなかったことにありました。具体的には、GOPATH
内のパッケージが依存する別の GOPATH
内のパッケージをビルドする際、競合検出器が有効になっているにもかかわらず、通常のビルドパスと同じ場所に中間ファイルを生成しようとしていました。これにより、競合検出器用のインストゥルメンテーションが施されたバージョンとそうでないバージョンが混在したり、期待されるインストゥルメンテーションが適用されていないパッケージがリンクされたりして、ビルドエラーや予期せぬランタイムエラーが発生していました。
このコミットは、このようなパスの衝突を避け、競合検出器が有効な場合に常に適切なインストゥルメンテーションが施されたパッケージが使用されるように、ビルドパスに _race
サフィックスを追加することでこの問題を解決します。
前提知識の解説
Go言語のビルドプロセスとcmd/go
Go言語のビルドは、主に go build
コマンドによって行われます。このコマンドは、ソースコードをコンパイルし、実行可能なバイナリを生成します。ビルドプロセスでは、依存関係の解決、パッケージのコンパイル、リンクといった一連のステップが実行されます。
GOPATH
: Go 1.11以前のバージョンでは、GOPATH
はGoのワークスペースのルートディレクトリを指定する重要な環境変数でした。ソースコード、コンパイル済みパッケージ、実行可能バイナリがこのディレクトリ構造内に配置されます。GOPATH
内のパッケージは、src/
ディレクトリ以下に配置され、go build
はこのパスを基に依存パッケージを探索します。Go Modulesの導入によりGOPATH
の重要性は低下しましたが、このコミットが作成された2012年当時はGo開発の標準的なワークフローでした。- パッケージ解決:
go build
は、インポートパスに基づいて必要なパッケージを探索します。標準ライブラリのパッケージはGoのインストールディレクトリから、サードパーティ製パッケージやユーザー自身のパッケージはGOPATH
から探索されます。 - 中間ファイルとキャッシュ: ビルドプロセス中、Goツールチェーンはコンパイル済みパッケージの中間ファイル(
.a
ファイルなど)を生成し、これらをキャッシュしてビルド時間を短縮します。これらのファイルは通常、pkg/
ディレクトリ以下に、OSとアーキテクチャ (goos_goarch
) に応じたサブディレクトリに保存されます。
Go言語の競合検出器 (Race Detector)
Go言語の競合検出器は、Go 1.1で導入された強力なデバッグツールです。プログラムの実行中に発生する可能性のあるデータ競合を動的に検出します。
- データ競合: 複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合は、プログラムの動作を非決定論的にし、デバッグが困難なバグの原因となります。
- 有効化: 競合検出器は、
go build
、go run
、go test
などのコマンドに-race
フラグを追加することで有効にできます。例:go build -race myapp.go
- 動作原理: 競合検出器は、コンパイル時にコードに特別なインストゥルメンテーションを挿入することで機能します。このインストゥルメンテーションは、メモリへのアクセス(読み書き)とゴルーチンの同期イベントを追跡します。実行時に、これらのイベントを監視し、データ競合のパターンを検出すると警告を出力します。
- パフォーマンスへの影響: 競合検出器を有効にすると、インストゥルメンテーションのオーバーヘッドにより、プログラムの実行速度が低下し、メモリ使用量が増加します。そのため、通常は開発およびテスト段階でのみ使用されます。
- ビルド成果物の違い: 競合検出器が有効な場合、生成されるバイナリは通常のバイナリとは異なります。これは、追加のインストゥルメンテーションコードが含まれているためです。したがって、競合検出器を有効にしてビルドされたパッケージは、そうでないパッケージとは異なるものとして扱われる必要があります。
技術的詳細
このコミットが修正する問題は、Goのビルドシステムが、競合検出器を有効にしたビルド (-race
フラグ付き) と通常のビルドで、コンパイル済みパッケージのキャッシュパスを適切に分離していなかったことに起因します。
Goのビルドツール (cmd/go
) は、コンパイル済みパッケージを GOPATH/pkg/<OS_ARCH>/
のようなディレクトリにキャッシュします。ここで <OS_ARCH>
は、例えば linux_amd64
のように、ターゲットのオペレーティングシステムとアーキテクチャを示します。
競合検出器を有効にしてビルドする場合、コンパイラはコードに特別なインストゥルメンテーションを挿入します。これにより、生成されるオブジェクトファイルやライブラリは、通常のビルドで生成されるものとは互換性がなくなります。もし、競合検出器が有効なビルドとそうでないビルドが同じキャッシュディレクトリを使用しようとすると、以下の問題が発生します。
- キャッシュの衝突: 競合検出器が有効なビルドが、通常のビルドによってキャッシュされたパッケージを上書きしてしまう可能性があります。その逆も同様です。
- 不適切なリンク:
GOPATH
内のパッケージAがGOPATH
内のパッケージBをインポートしている場合を考えます。- まず、パッケージBが通常のビルドでコンパイルされ、キャッシュされます。
- 次に、パッケージAが
-race
フラグ付きでビルドされます。この際、パッケージAはパッケージBに依存しているため、キャッシュされたパッケージBを探します。 - もし、競合検出器が有効なビルド用のパッケージBがキャッシュに存在しない場合、通常のビルドでキャッシュされたパッケージB(インストゥルメンテーションなし)が誤ってリンクされてしまう可能性があります。これにより、パッケージAの競合検出器が正しく機能しなかったり、ビルドエラーが発生したりします。
このコミットは、この問題を解決するために、競合検出器が有効な場合にのみ、キャッシュディレクトリのパスに _race
サフィックスを追加するように変更します。例えば、linux_amd64
の代わりに linux_amd64_race
のようなディレクトリを使用します。これにより、競合検出器が有効なビルドとそうでないビルドのキャッシュが完全に分離され、互いに干渉することがなくなります。結果として、常に適切なインストゥルメンテーションが施されたパッケージがリンクされるようになり、ビルドの失敗や競合検出器の誤動作が解消されます。
コアとなるコードの変更箇所
変更は src/cmd/go/build.go
ファイルの includeArgs
関数内で行われています。
--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -876,6 +876,9 @@ func (b *builder) includeArgs(flag string, all []*action) []string {
dir = filepath.Join(dir, "gccgo")
} else {
dir = filepath.Join(dir, goos+"_"+goarch)
+ if buildRace {
+ dir += "_race"
+ }
}
inc = append(inc, flag, dir)
}
コアとなるコードの解説
この変更は、src/cmd/go/build.go
内の builder
構造体の includeArgs
メソッドにあります。このメソッドは、ビルドプロセス中にコンパイラに渡されるインクルードパス引数を構築する役割を担っています。
元のコードでは、コンパイル済みパッケージのキャッシュディレクトリは、オペレーティングシステム (goos
) とアーキテクチャ (goarch
) に基づいて goos + "_" + goarch
の形式で決定されていました(例: linux_amd64
)。
追加された3行のコードは以下の通りです。
if buildRace {
dir += "_race"
}
buildRace
は、Goコマンドが-race
フラグ付きで呼び出されたかどうかを示すブール型の変数です。この変数がtrue
の場合、競合検出器が有効になっていることを意味します。if buildRace
の条件文は、競合検出器が有効な場合にのみ、以下の処理を実行するようにします。dir += "_race"
: ここがこのコミットの核心です。既存のディレクトリパス (dir
) に文字列_race
を追加しています。これにより、例えばlinux_amd64
だったパスがlinux_amd64_race
に変更されます。
この変更により、競合検出器が有効なビルドでは、コンパイル済みパッケージや中間ファイルが GOPATH/pkg/linux_amd64_race/
のような、通常のビルドとは異なる専用のディレクトリにキャッシュされるようになります。これにより、通常のビルドのキャッシュと競合検出器が有効なビルドのキャッシュが完全に分離され、互いに上書きし合うことがなくなります。結果として、GOPATH
内のパッケージが互いに依存している場合でも、競合検出器を有効にしたビルドが正しく行われるようになります。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/fb9706d3bed364276c075081fbab820719fc5965
- Gerrit Code Review (Goの公式コードレビューシステム): https://golang.org/cl/6811083
参考にした情報源リンク
- Go Race Detector: https://go.dev/blog/race-detector
- Go Command Documentation: https://go.dev/cmd/go/
- GOPATH (Go 1.11以前のドキュメント): https://go.dev/doc/gopath_code (現在のGo Modulesのドキュメントとは異なりますが、当時の状況を理解するために参照)
filepath.Join
documentation: https://pkg.go.dev/path/filepath#Join- Go言語のビルドシステムに関する一般的な情報 (Go Modules以前の文脈): https://go.dev/doc/code (当時のGoのコード構成とビルドに関する基本的な理解のために参照)