Read OSS

Go リポジトリを読み解く:ディレクトリ構成、ブートストラップ、ビルドパイプライン

中級

前提知識

  • Go の基本的な文法とツールへの慣れ
  • コンパイラツールチェーンが何であるかの理解

Go リポジトリを読み解く:ディレクトリ構成、ブートストラップ、ビルドパイプライン

golang/go リポジトリは、現代のソフトウェアエンジニアリングにおいて最も重要なコードベースのひとつです。Go コンパイラ、リンカ、runtime、標準ライブラリ、そして go コマンド本体が一堂に収まっており、Go、アセンブリ、C 合わせておよそ 150 万行のコードが完全にセルフホスティングなツールチェーンを形成しています。その規模にもかかわらず、リポジトリは驚くほどフラットで規律ある構成に従っています。この記事ではその構成を整理し、Go がゼロからどのように自分自身をビルドするかを追います。

トップレベルのディレクトリ構成

多くの大規模プロジェクトが数十のマイクロサービスや深いネストのモジュール構成を取るのに対し、Go リポジトリはシングルモジュールとしてシンプルな階層にまとめられています。Go ディストリビューションとして配布されるすべてのものは src/ 以下に置かれています。

ディレクトリ 役割
src/ すべての Go ソースコード:標準ライブラリ、ツールチェーンコマンド、runtime
src/cmd/ ツールチェーンコマンド:gocompilelinkasmvetgofmtdist
src/runtime/ Go runtime:スケジューラ、メモリアロケータ、ガベージコレクタ、OS 抽象化層
src/internal/ 標準ライブラリ内で共有される internal パッケージ(ユーザーには非公開)
api/ Go 1 互換性保証のための API 互換性追跡ファイル
doc/ ドキュメント、リリースノート、設計ドキュメント
test/ コンパイラおよび runtime のエンドツーエンドテスト
lib/ プリビルドされたタイムゾーンおよび Unicode データ
misc/ エディタサポート、プラットフォーム固有のファイル、補助ツール

モジュール定義は一見シンプルです。

src/go.mod#L1-L13

module std

fmtnet/httpcrypto といった標準ライブラリのパッケージはすべて std という名前の単一モジュールに属しています。これは重要な設計上の選択です。標準ライブラリの全パッケージが一括してバージョン管理・リリースされ、モジュール境界をまたいだ内部的な依存解決も発生しません。外部依存は vendoring された golang.org/x/ パッケージのみです。

ヒント: Go のソースを読む際は、src/cmd/ 配下のパッケージが src/cmd/go.mod という別モジュールを使っている点に注意してください。これにより、ツールチェーンは標準ライブラリとは異なる依存関係を持てるようになっています。

ブートストラップビルドの仕組み

Go はセルフホスティング言語です。つまり、Go コンパイラをビルドするには、動作する Go コンパイラが必要になります。ソースからのビルドのエントリーポイントは make.bash というシェルスクリプトで、この循環的な依存関係をうまく解決するよう慎重に設計されています。

スクリプトは環境の検証と安全確認から始まり、その後ひとつの重要なタスクに集中します。ブートストラップコンパイラを使って cmd/dist をビルドすることです。

src/make.bash#L67-L74

ブートストラップに必要な最低バージョンは Go 1.24.6 です。スクリプトはまず $GOROOT_BOOTSTRAP からブートストラップ用ツールチェーンを探します。見つからなければ $HOME/go1.24.6$HOME/sdk/go1.24.6$HOME/go1.4 の順にフォールバックします。最後のパスは、ハードコードされたビルドスクリプトとの後方互換性のために残されているレガシーパスです。

実際のビルドはたった 2 つのコマンドで完結します。

src/make.bash#L194-L219

まずブートストラップコンパイラが cmd/dist をビルドし、次に cmd/dist bootstrap が残りのすべて——新しいコンパイラ、リンカ、アセンブラ、標準ライブラリ——をビルドします。最後のコメントには「DO NOT ADD ANY NEW CODE HERE.(ここに新しいコードを追加しないこと)」と強調されています。ビルドロジックはすべて cmd/dist に置くべきであり、make.bashmake.batmake.rc の 3 箇所で同じコードを管理する事態を避けるためです。

flowchart TD
    A["make.bash starts"] --> B["Validate environment<br/>(GOROOT, GOARCH, etc.)"]
    B --> C["Find bootstrap Go ≥ 1.24.6"]
    C --> D["Bootstrap compiler builds cmd/dist"]
    D --> E["cmd/dist bootstrap -a"]
    E --> F["Build new compiler (cmd/compile)"]
    E --> G["Build new linker (cmd/link)"]
    E --> H["Build new assembler (cmd/asm)"]
    F --> I["Build standard library with new toolchain"]
    G --> I
    H --> I
    I --> J["Toolchain ready in GOROOT/pkg/tool/"]

cmd/dist:最初にビルドされるバイナリ

cmd/dist はブートストラップのオーケストレーターです。古いツールチェーンでもコンパイルできるよう、意図的にシンプルな Go で書かれています。エントリーポイントを見ると、すっきりしたコマンドディスパッチのパターンがわかります。

src/cmd/dist/main.go#L34-L43

var commands = map[string]func(){
    "banner":    cmdbanner,
    "bootstrap": cmdbootstrap,
    "clean":     cmdclean,
    "env":       cmdenv,
    "install":   cmdinstall,
    "list":      cmdlist,
    "test":      cmdtest,
    "version":   cmdversion,
}

make.bash が呼び出すのは bootstrap コマンドです。この関数がマルチステージビルドのオーケストレーションを担い、まずツールチェーンバイナリをビルドし、続いて出来立てのツールで標準ライブラリをコンパイルします。

main() 関数はプラットフォーム検出も担います。Go が幅広いプラットフォームをサポートしていることを考えると、これは単純な処理ではありません。uname を使ってホストアーキテクチャを検出しますが、プロセスツリーに x86 の親プロセスが存在する場合に x86_64 と報告してしまう macOS ARM64 マシンのようなエッジケースも適切に処理しています。

src/cmd/dist/main.go#L86-L146

flowchart LR
    A["cmdbootstrap()"] --> B["Build cmd/compile"]
    B --> C["Build cmd/link"]
    C --> D["Build cmd/asm"]
    D --> E["Build cmd/go"]
    E --> F["Compile standard library"]
    F --> G["Install to GOTOOLDIR"]

API 互換性とリリース管理

api/ ディレクトリは、Go 1 互換性保証——Go 1.0 向けに書かれたコードがすべての将来の Go 1.x リリースでも正しくコンパイル・実行できるという約束——を強制するための仕組みです。

各リリースには対応する api/go1.N.txt ファイルがあり、公開 API の全体像がリストアップされています。エクスポートされた型、関数、メソッド、定数、変数がすべて含まれます。ベースファイルである api/go1.txt には、Go 1.0 時点の元々の API が定義されています。

api/go1.txt#L1-L20

各行は pkg <package>, <kind> <name> <type> という構造化されたフォーマットに従っています。go ツールの API チェッカーは現在のソースをこれらのファイルと比較し、意図しない API の削除を防ぎます。開発中に追加される新しい API は api/next/ で追跡され、リリース時にバージョン付きのファイルに固定されます。

ヒント: Go に新しい公開 API を追加するコントリビューションをする場合は、api/next/ 内のファイルにその API を追記する必要があります。src/cmd/gogo generate ステップでこれらのファイルの整合性が検証されます。

このアプローチはローテクなものです——バージョン管理されたプレーンテキストファイルにすぎません——しかし驚くほど効果的です。API の変更をコードレビューで可視化し、エコシステムの何千もの Go パッケージが意図せず壊れる事態を防いでいます。

ツールチェーンコマンドの全体像

src/cmd/ ディレクトリには、Go に同梱されるすべてのツールが含まれています。それぞれ同じアーキテクチャパターンに従っており、薄い main.gointernal/ パッケージにディスパッチし、実際の実装はそこに置かれています。

graph TD
    GO["cmd/go<br/>User-facing CLI"] -->|"invokes"| COMPILE["cmd/compile<br/>Go → object files"]
    GO -->|"invokes"| LINK["cmd/link<br/>object files → binary"]
    GO -->|"invokes"| ASM["cmd/asm<br/>assembly → object files"]
    GO -->|"invokes"| VET["cmd/vet<br/>static analysis"]
    COMPILE --> OBJ["*.o object files"]
    ASM --> OBJ
    OBJ --> LINK
    LINK --> BIN["executable binary"]

cmd/go はユーザーが直接触れる主要ツールです。buildtestmodrun といったサブコマンドをディスパッチし、コンパイラとリンカをサブプロセスとして起動することでビルドプロセス全体を調整します。

src/cmd/go/main.go#L50-L92

cmd/compile は Go コンパイラです。その main.go は驚くほど簡潔で、archInits マップでアーキテクチャ固有の初期化処理を選択し、gc.Main に処理を委譲します。

src/cmd/compile/main.go#L28-L59

cmd/link も同じパターンに従っていますが、マップの代わりに switch 文を使い、アーキテクチャ固有の Init() 関数にディスパッチした後、ld.Main を呼び出します。

src/cmd/link/main.go#L40-L73

薄いエントリーポイントにアーキテクチャディスパッチを組み合わせるこのパターンは随所に見られます。コアロジックをアーキテクチャ非依存に保ちつつ、明確に定義されたインターフェースを通じて各ターゲットが振る舞いをカスタマイズできる構造になっています。

次のステップ

全体の地図が頭に入ったところで、いよいよ各コンポーネントの詳細に踏み込んでいきましょう。次の記事では go コマンドの内部アーキテクチャを掘り下げます。サブコマンドがどのように登録・ディスパッチされるか、go build がどのように依存グラフを構築して並列コンパイルを調整するか、そして go.mod のディレクティブに基づいて透過的に Go バージョンを切り替えるツールチェーン選択の仕組みについて見ていきます。