[インデックス 12410] ファイルの概要
このコミットは、Go言語のビルドシステムにおけるパッケージ間の依存関係をテストするための新しいファイル src/pkg/go/build/deps_test.go を追加します。このテストは、go/build パッケージの Import 関数を検証するだけでなく、Go標準ライブラリ内のパッケージ依存関係に関する公式なポリシーを文書化し、その遵守を強制することを目的としています。これにより、予期せぬ依存関係の発見と、それに基づく調整が促されます。
コミット
commit 88e86936be8eb4d2c2ed8a0ad4c74d743dff1cc9
Author: Russ Cox <rsc@golang.org>
Date: Mon Mar 5 23:13:00 2012 -0500
go/build: add dependency test
This exercises the Import function but more importantly
gives us a place to write down the policy for dependencies
within the Go tree. It also forces us to look at the dependencies,
which may lead to adjustments.
Surprises:
- go/doc imports text/template, for HTMLEscape (could fix)
- it is impossible to use math/big without fmt (unfixable)
- it is impossible to use crypto/rand without math/big (unfixable)
R=golang-dev, bradfitz, gri, r
CC=golang-dev
https://golang.org/cl/5732062
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/88e86936be8eb4d2c2ed8a0ad4c74d743dff1cc9
元コミット内容
go/build: add dependency test
This exercises the Import function but more importantly
gives us a place to write down the policy for dependencies
within the Go tree. It also forces us to look at the dependencies,
which may lead to adjustments.
Surprises:
- go/doc imports text/template, for HTMLEscape (could fix)
- it is impossible to use math/big without fmt (unfixable)
- it is impossible to use crypto/rand without math/big (unfixable)
R=golang-dev, bradfitz, gri, r
CC=golang-dev
https://golang.org/cl/5732062
変更の背景
Go言語のような大規模なプロジェクトにおいて、標準ライブラリ内のパッケージ間の依存関係を管理することは非常に重要です。不適切な依存関係は、以下のような問題を引き起こす可能性があります。
- 循環参照 (Circular Dependencies): パッケージAがパッケージBに依存し、パッケージBがパッケージAに依存するような状況は、コードの理解を困難にし、コンパイルエラーや予期せぬ動作の原因となります。
- 肥大化 (Bloat): 特定の機能のために不要なパッケージがインポートされると、バイナリサイズが増加し、コンパイル時間や実行時のメモリ使用量に悪影響を与えます。
- 保守性の低下: 依存関係が複雑になると、コードの変更が他の部分に与える影響を予測しにくくなり、バグの導入リスクが高まります。
- レイヤー違反: 低レベルのパッケージが高レベルのパッケージに依存するなど、設計上のレイヤー構造が崩れると、システムのアーキテクチャが損なわれ、拡張性や再利用性が低下します。
このコミットが追加された背景には、Go開発チームが標準ライブラリの健全性を維持し、将来的な開発を円滑に進めるために、これらの問題を未然に防ぐための明確なポリシーと検証メカニズムが必要であるという認識がありました。特に、go/build パッケージの Import 関数は、Goのビルドプロセスにおいてパッケージのインポートパスを解決する中心的な役割を担っており、この関数の動作をテストすることは、ビルドシステムの堅牢性を保証する上で不可欠です。
また、コミットメッセージにある「Surprises」の記述は、実際に依存関係を調査した際に発見された予期せぬ、あるいは望ましくない依存関係の存在を示唆しています。これらの発見は、依存関係テストの導入が単なる形式的なものではなく、実際のコードベースの健全性向上に寄与することを示しています。このテストは、開発者が新しいコードを追加する際に、既存の依存関係ポリシーに違反していないかを自動的にチェックする仕組みを提供し、Goエコシステム全体の品質を向上させることを目指しています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびソフトウェア開発に関する基本的な概念を理解しておく必要があります。
-
Goパッケージとインポートパス:
- Goのコードは「パッケージ」という単位で組織されます。各パッケージは、関連する機能の集合体です。
- 他のパッケージの機能を利用するには、
importステートメントを使用してそのパッケージをインポートします。 - インポートパスは、パッケージを一意に識別するための文字列です(例:
"fmt","net/http","go/build")。
-
go/buildパッケージ:- Go標準ライブラリの一部であり、Goソースコードのビルドプロセスに関する情報を提供します。
go/build.Context構造体は、ビルド環境(OS、アーキテクチャ、Cgoの有効/無効など)に関する設定を保持します。Context.Import(path, srcDir, mode)関数は、指定されたインポートパスpathに対応するパッケージを検索し、そのパッケージに関する情報(インポートする他のパッケージのリストなど)を返します。このテストでは、このImport関数の動作を検証し、パッケージが実際にインポートする依存関係を特定するために使用されます。
-
依存関係管理:
- ソフトウェアプロジェクトにおける依存関係管理とは、プロジェクトが正しく機能するために必要な外部ライブラリやモジュールを特定し、取得し、管理するプロセスです。
- Goにおいては、
go.modファイルとGo Modulesが現代的な依存関係管理の主流ですが、このコミットが作成された2012年当時は、まだGo Modulesは存在せず、GOPATHベースのワークスペースと、標準ライブラリ内のパッケージ間の暗黙的な依存関係が主な関心事でした。 - このテストは、特にGo標準ライブラリ内部のパッケージが、どの他の標準ライブラリパッケージに依存して良いか、という「ポリシー」を明示的に定義し、検証するものです。
-
レイヤー化されたアーキテクチャ:
- 大規模なソフトウェアシステムでは、機能を論理的な層(レイヤー)に分割することが一般的です。例えば、低レベルのユーティリティ層、データ構造層、I/O層、ネットワーク層などです。
- 理想的には、高レベルの層は低レベルの層に依存しますが、低レベルの層が高レベルの層に依存することは避けるべきです(依存関係の逆転)。
- このコミットで導入される
pkgDepsマップ内のL0,L1,L2,L3といった定義は、Go標準ライブラリにおけるパッケージの「レイヤー」を明示し、各レイヤーがどの他のレイヤーに依存して良いかというポリシーを表現しています。
-
Goのテストフレームワーク (
testingパッケージ):- Goには、ユニットテストやベンチマークテストを記述するための組み込みの
testingパッケージがあります。 - テスト関数は
Testで始まり、*testing.T型の引数を取ります。 t.Errorf()はテスト失敗を報告し、t.Logf()はテスト中にログメッセージを出力します。testing.Short()は、go test -shortコマンドが実行された場合にtrueを返し、時間のかかるテストをスキップするために使用されます。
- Goには、ユニットテストやベンチマークテストを記述するための組み込みの
これらの知識を前提として、deps_test.go がどのようにGo標準ライブラリの依存関係ポリシーを定義し、検証しているかを深く掘り下げていきます。
技術的詳細
src/pkg/go/build/deps_test.go は、Go標準ライブラリのパッケージ依存関係を検証するためのテストファイルです。その核心は、pkgDeps というグローバルマップ変数にあります。
pkgDeps マップ
pkgDeps は map[string][]string 型で、Go標準ライブラリ内のパッケージ間の期待される依存関係を定義しています。これは「ポリシーの表明」であり、「このデータをビルドを修正するために変更してはならない」という強い警告がコメントに記されています。
マップのエントリには2種類あります。
- 小文字のキー: 標準のインポートパス(例:
"errors","io","fmt")を表し、そのパッケージが許可されているインポートのリストを値として持ちます。 - 大文字のキー: パッケージセットのエイリアス(例:
"L0","L1","OS","NET","CRYPTO")を定義します。これらのエイリアスは、他のルールによって依存関係として使用できます。これにより、依存関係の階層構造を表現し、繰り返しを避けることができます。
依存関係の階層 (L0, L1, L2, L3)
pkgDeps マップは、Go標準ライブラリのパッケージを論理的な「レイヤー」に分割し、低レベルから高レベルへの依存関係を定義しています。
- L0 (Lowest Level):
errors,io,runtime,sync,sync/atomic,unsafe。これらは「コアで、ほとんど避けられないパッケージ」と定義されており、他のパッケージが最も基本的な機能を提供するために依存する基盤です。 - L1 (Simple Data and Functions):
bufio,bytes,math,math/cmplx,math/rand,path,sort,strconv,strings,unicode,unicode/utf16,unicode/utf8。L0に加えて、Unicodeや文字列処理などの基本的なデータ型と関数を追加します。 - L2 (Reflection and Basic Utility):
crypto,crypto/cipher,encoding/base32,encoding/base64,encoding/binary,hash,hash/adler32,hash/crc32,hash/crc64,hash/fnv,image,image/color,reflect。L1に加えて、リフレクションや基本的なユーティリティパッケージ、インターフェース定義を含みますが、システムコールを行うものは含まれません。 - L3 (L2 + fmt + log + time):
L2,fmt,log,time。L2パッケージを使用する際には、fmt(フォーマットI/O),log(ロギング),time(時間操作) の使用は「大した問題ではない」と見なされるため、これらをまとめたレイヤーです。
その他の主要なエイリアス
- OS:
io/ioutil,os,os/exec,path/filepath,time。基本的なオペレーティングシステム機能へのアクセスを可能にしますが、syscallパッケージの直接使用やos/signalは含みません。 - GOPARSER:
go/ast,go/doc,go/parser,go/printer,go/scanner,go/token。Goのパーサー関連パッケージをまとめたものです。 - CGO:
C,runtime/cgo。Cgo(GoとC言語の相互運用)関連のパッケージです。 - NET:
net,mime,net/textproto,net/url。基本的なネットワーク関連パッケージをまとめたものです。 - CRYPTO:
crypto/aes,crypto/des,crypto/hmac,crypto/md5,crypto/rc4,crypto/sha1,crypto/sha256,crypto/sha512,crypto/subtle。コアな暗号化パッケージです。 - CRYPTO-MATH:
CRYPTO,crypto/dsa,crypto/ecdsa,crypto/elliptic,crypto/rand,crypto/rsa,encoding/asn1,math/big。数学的な暗号化パッケージで、math/bigへの依存を含みます。
isMacro 関数
func isMacro(p string) bool {
return 'A' <= p[0] && p[0] <= 'Z'
}
このヘルパー関数は、与えられた文字列 p が大文字で始まるかどうかをチェックし、それが pkgDeps マップ内のマクロ(パッケージセットのエイリアス)であるかどうかを判断します。
allowed 関数
func allowed(pkg string) map[string]bool {
m := map[string]bool{}
var allow func(string)
allow = func(p string) {
if m[p] {
return
}
m[p] = true // set even for macros, to avoid loop on cycle
// Upper-case names are macro-expanded.
if isMacro(p) {
for _, pp := range pkgDeps[p] {
allow(pp)
}
}
}
for _, pp := range pkgDeps[pkg] {
allow(pp)
}
return m
}
この関数は、特定のパッケージ pkg が直接的または間接的にインポートすることを許可されているすべてのパッケージのセットを計算します。再帰的に pkgDeps マップを辿り、マクロを展開して、許可されたすべての依存関係を map[string]bool 形式で返します。循環参照を避けるために、既に処理中のパッケージはスキップされます。
TestDependencies 関数
func TestDependencies(t *testing.T) {
var all []string
for k := range pkgDeps {
all = append(all, k)
}
sort.Strings(all)
ctxt := build.Default
test := func(mustImport bool) {
for _, pkg := range all {
if isMacro(pkg) {
continue
}
p, err := ctxt.Import(pkg, "", 0)
if err != nil {
// Some of the combinations we try might not
// be reasonable (like arm,plan9,cgo), so ignore
// errors for the auto-generated combinations.
if !mustImport {
continue
}
t.Errorf("%s/%s/cgo=%v %v", ctxt.GOOS, ctxt.GOARCH, ctxt.CgoEnabled, err)
continue
}
ok := allowed(pkg)
var bad []string
for _, imp := range p.Imports {
if !ok[imp] {
bad = append(bad, imp)
}
}
if bad != nil {
t.Errorf("%s/%s/cgo=%v unexpected dependency: %s imports %v", ctxt.GOOS, ctxt.GOARCH, ctxt.CgoEnabled, pkg, bad)
}
}
}
test(true) // Run with default context, must succeed
if testing.Short() {
t.Logf("skipping other systems")
return
}
// Test across various OS, architectures, and Cgo settings
for _, ctxt.GOOS = range geese {
for _, ctxt.GOARCH = range goarches {
for _, ctxt.CgoEnabled = range bools {
test(false) // Allow import errors for unreasonable combinations
}
}
}
}
このテスト関数は、pkgDeps マップに定義された依存関係ポリシーを実際に検証します。
- パッケージの列挙:
pkgDepsマップのすべてのキー(パッケージ名とマクロ名)を抽出し、ソートします。 - デフォルトコンテキストでのテスト:
build.Defaultコンテキスト(現在のシステム環境)を使用して、すべての非マクロパッケージについてループします。ctxt.Import(pkg, "", 0)を呼び出して、Goのビルドシステムが実際にそのパッケージがインポートする依存関係のリスト (p.Imports) を取得します。allowed(pkg)関数を使って、pkgDepsポリシーに基づいてそのパッケージがインポートを許可されているすべてのパッケージのセットを取得します。p.Importsに含まれる実際の依存関係が、allowed(pkg)で許可されたセットに含まれているかをチェックします。- もし許可されていない依存関係が見つかった場合、
t.Errorfを使ってテストエラーを報告します。
- クロスプラットフォーム/アーキテクチャテスト:
testing.Short()がfalseの場合(つまり、go test -shortが指定されていない場合)、テストはさらに広範な環境で実行されます。geese(OSのリスト: darwin, freebsd, linux, netbsd, openbsd, plan9, windows) とgoarches(アーキテクチャのリスト: 386, amd64, arm) のすべての組み合わせ、およびCgoEnabled(true/false) の組み合わせでループします。- 各組み合わせで
build.DefaultコンテキストのGOOS,GOARCH,CgoEnabledを設定し、再度test(false)を呼び出します。 test(false)の場合、ctxt.Importがエラーを返しても、それが「不合理な組み合わせ」(例: arm, plan9, cgo)によるものであれば、テストエラーとはせずにスキップします。これは、すべてのOS/アーキテクチャの組み合わせで全てのパッケージがビルド可能である必要はないためです。
コミットメッセージの「Surprises」について
コミットメッセージに記載されている「Surprises」は、この依存関係テストの導入によって実際に発見された、予期せぬ、あるいは望ましくない依存関係の例です。
go/docimportstext/template, forHTMLEscape(could fix):go/docパッケージはGoのドキュメント生成に関連するパッケージです。text/templateはテキストテンプレートエンジンです。HTMLEscapeはHTMLエスケープ処理を行う関数です。- この依存関係は、
go/docがドキュメントをHTML形式で出力する際にtext/templateの機能を利用していることを示唆しています。コミットメッセージでは「修正可能」とされており、これはgo/docがtext/templateに依存せずにHTMLエスケープ処理を行う、あるいはより低レベルのユーティリティに依存するようにリファクタリングできる可能性を示しています。
- it is impossible to use
math/bigwithoutfmt(unfixable):math/bigは任意精度整数/浮動小数点数演算を提供するパッケージです。fmtはフォーマットされたI/O(Printfなど)を提供するパッケージです。- この記述は、
math/bigが内部的にfmtパッケージに依存しているため、math/bigを使用するプログラムは必然的にfmtもインポートすることになる、という事実を指摘しています。コミットメッセージでは「修正不可能」とされており、これはGoの設計上、math/bigの機能を実現するためにfmtが不可欠であるか、あるいはその依存関係を解消することが非常に困難であることを意味します。これは、math/bigが数値の文字列変換などでfmtの機能を利用しているためと考えられます。
- it is impossible to use
crypto/randwithoutmath/big(unfixable):crypto/randは暗号学的に安全な乱数生成器を提供するパッケージです。- この記述は、
crypto/randがmath/bigに依存しているため、crypto/randを使用するプログラムは必然的にmath/bigもインポートすることになる、という事実を指摘しています。これも「修正不可能」とされており、暗号学的な乱数生成において、大きな数値の演算が必要となるため、math/bigが不可欠であると考えられます。
これらの「Surprises」は、依存関係テストが単にポリシーを強制するだけでなく、既存のコードベースにおける隠れた、あるいは予期せぬ依存関係を発見し、その設計上のトレードオフを明確にする上で非常に有用であることを示しています。
コアとなるコードの変更箇所
このコミットでは、以下の新しいファイルが追加されました。
src/pkg/go/build/deps_test.go(403行の追加)
このファイルは、Go標準ライブラリのパッケージ依存関係を検証するためのテストコードを含んでいます。
コアとなるコードの解説
追加された src/pkg/go/build/deps_test.go ファイルの主要な構成要素は以下の通りです。
-
パッケージ宣言とインポート:
package build_test import ( "go/build" "sort" "testing" )build_testパッケージとして定義されており、go/buildパッケージ(テスト対象)、sortパッケージ(パッケージリストのソート用)、testingパッケージ(Goのテストフレームワーク)をインポートしています。 -
pkgDepsマップ: このファイルの中心となるデータ構造です。Go標準ライブラリ内の各パッケージがインポートを許可されている他のパッケージを定義します。前述の「技術的詳細」セクションで詳しく説明したように、小文字のキーで個々のパッケージの依存関係を、大文字のキーでパッケージのグループ(L0, L1など)を定義しています。このマップは、Go標準ライブラリの依存関係ポリシーをコードとして表現したものです。 -
isMacro関数:func isMacro(p string) bool { return 'A' <= p[0] && p[0] <= 'Z' }文字列の最初の文字が大文字かどうかで、それが
pkgDepsマップ内のマクロ(パッケージセットのエイリアス)であるかを判定するシンプルなヘルパー関数です。 -
allowed関数:func allowed(pkg string) map[string]bool { // ... (実装は技術的詳細セクションを参照) }特定のパッケージ
pkgが、pkgDepsマップの定義に基づいて、直接的または間接的にインポートすることを許可されているすべてのパッケージのセットを計算します。マクロを展開し、再帰的に依存関係を解決します。 -
TestDependencies関数:func TestDependencies(t *testing.T) { // ... (実装は技術的詳細セクションを参照) }Goのテスト関数であり、
pkgDepsマップに定義された依存関係ポリシーを検証するメインロジックを含みます。build.Defaultコンテキストを使用して、各パッケージが実際にインポートする依存関係 (p.Imports) を取得します。allowed関数が返す許可された依存関係のセットと、実際の依存関係を比較します。- 許可されていない依存関係が見つかった場合、
t.Errorfを使ってテストエラーを報告します。 geese(OS) とgoarches(アーキテクチャ) の組み合わせをループすることで、様々なビルド環境下での依存関係も検証します。これにより、プラットフォーム固有の依存関係の問題も検出できるようになっています。
このテストファイルは、Go標準ライブラリの依存関係の健全性を自動的にチェックし、開発者が意図しない依存関係を導入するのを防ぐための重要なガードレールとして機能します。
関連リンク
- Gerrit Change-ID:
https://golang.org/cl/5732062(GoプロジェクトのコードレビューシステムであるGerrit上の変更セットへのリンク)
参考にした情報源リンク
- Go言語公式ドキュメント: https://go.dev/doc/
go/buildパッケージドキュメント: https://pkg.go.dev/go/buildtestingパッケージドキュメント: https://pkg.go.dev/testing- Go Modules (現代のGoの依存関係管理): https://go.dev/blog/using-go-modules (このコミット時点では存在しませんでしたが、依存関係管理の文脈で参考になります)
- Goのパッケージとモジュールに関する一般的な情報
- ソフトウェアアーキテクチャにおけるレイヤー化の原則
- 循環参照に関する一般的なプログラミングの概念