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

[インデックス 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 buildgo rungo test などのコマンドに -race フラグを追加することで有効にできます。例: go build -race myapp.go
  • 動作原理: 競合検出器は、コンパイル時にコードに特別なインストゥルメンテーションを挿入することで機能します。このインストゥルメンテーションは、メモリへのアクセス(読み書き)とゴルーチンの同期イベントを追跡します。実行時に、これらのイベントを監視し、データ競合のパターンを検出すると警告を出力します。
  • パフォーマンスへの影響: 競合検出器を有効にすると、インストゥルメンテーションのオーバーヘッドにより、プログラムの実行速度が低下し、メモリ使用量が増加します。そのため、通常は開発およびテスト段階でのみ使用されます。
  • ビルド成果物の違い: 競合検出器が有効な場合、生成されるバイナリは通常のバイナリとは異なります。これは、追加のインストゥルメンテーションコードが含まれているためです。したがって、競合検出器を有効にしてビルドされたパッケージは、そうでないパッケージとは異なるものとして扱われる必要があります。

技術的詳細

このコミットが修正する問題は、Goのビルドシステムが、競合検出器を有効にしたビルド (-race フラグ付き) と通常のビルドで、コンパイル済みパッケージのキャッシュパスを適切に分離していなかったことに起因します。

Goのビルドツール (cmd/go) は、コンパイル済みパッケージを GOPATH/pkg/<OS_ARCH>/ のようなディレクトリにキャッシュします。ここで <OS_ARCH> は、例えば linux_amd64 のように、ターゲットのオペレーティングシステムとアーキテクチャを示します。

競合検出器を有効にしてビルドする場合、コンパイラはコードに特別なインストゥルメンテーションを挿入します。これにより、生成されるオブジェクトファイルやライブラリは、通常のビルドで生成されるものとは互換性がなくなります。もし、競合検出器が有効なビルドとそうでないビルドが同じキャッシュディレクトリを使用しようとすると、以下の問題が発生します。

  1. キャッシュの衝突: 競合検出器が有効なビルドが、通常のビルドによってキャッシュされたパッケージを上書きしてしまう可能性があります。その逆も同様です。
  2. 不適切なリンク: 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 内のパッケージが互いに依存している場合でも、競合検出器を有効にしたビルドが正しく行われるようになります。

関連リンク

参考にした情報源リンク