モノリシックなワークスペースからモジュール化された明確さへ: Goの依存関係管理の進化を理解する
Min-jun Kim
Dev Intern · Leapcell

Goは、そのシンプルさ、並行性、および堅牢な標準ライブラリで知られていますが、当初はGOPATHを通じてプロジェクト構造と依存関係管理に対する独自のアプローチを示していました。単純なプロジェクトでは簡単でしたが、このモデルは言語が成熟し、プロジェクトが複雑になるにつれて限界を露呈しました。GOPATHからGo Modulesへの進化は、バージョニング、再現性、分離などの重要な問題に対処し、最新のソフトウェア開発のための強力なツールとしての地位を確立することにより、Goの開発における重要なマイルストーンとなります。
GOPATHの時代:集中型でシンプル(Go 1.0 - 1.10)
Go Modulesが登場する前は、GOPATHがGo開発の基礎でした。これは、すべてのGoソースコード、コンパイルされたパッケージ、および実行可能なバイナリが存在する単一のワークスペースを定義していました。
GOPATHの構造を理解する
GOPATHは、ディレクトリを指す環境変数であり、デフォルトでは $HOME/go であることがよくありました。このディレクトリ内で、Goは特定の構造を想定していました。
src/: すべてのソースファイルを含み、インポートパスで整理されていました。たとえば、github.com/user/projectは$GOPATH/src/github.com/user/projectに存在します。pkg/: より高速なビルドのために、コンパイルされたパッケージオブジェクト(例:.aファイル)を格納していました。bin/: コンパイルされた実行可能プログラムを保持していました。
GOPATH下でのワークフロー
go get github.com/some/package を使用すると、Goツールチェーンはパッケージのソースコードを $GOPATH/src/github.com/some/package に直接ダウンロードします。すべてのプロジェクトは、個々の要件に関係なく、この単一のダウンロードされたバージョンの依存関係を使用します。あなた自身のプロジェクトのソースコードも、goツールによって検出およびビルド可能にするために、$GOPATH/src/ 内に存在する必要がありました。
簡単なGOPATH時代のプロジェクト構造で説明しましょう。
$GOPATH
└── src
└── github.com
└── myuser
└── myproject
└── main.go
└── some_dependency
└── some_dependency.go # go getでダウンロード
一般的なWebフレームワークである github.com/gin-gonic/gin を使用する main.go を考えてみましょう。
// $GOPATH/src/github.com/myuser/myproject/main.go package main import ( "log" "net/http" "github.com/gin-gonic/gin" // 暗黙的に $GOPATH/src を探す ) func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "pong", }) }) log.Println("Server starting on :8080") r.Run(":8080") // 0.0.0.0:8080 でリッスンおよびサービスを提供 }
これをビルドして実行するには、$GOPATH/src/github.com/myuser/myproject/ に移動して、go build または go run . を実行します。
GOPATHの限界
単純ですが、GOPATHは、プロジェクトの複雑さが増すにつれてエスカレートするいくつかの重大な欠点に悩まされていました。
- バージョニングなし: すべてのプロジェクトは、
GOPATHにインストールされた依存関係のまったく同じバージョンを共有していました。プロジェクトAがpackage@1.0.0を必要とし、プロジェクトBがpackage@2.0.0を必要とする場合、GOPATH/srcには1つのバージョンしか存在できないため、「依存関係地獄」につながりました。これにより、再現可能なビルドが非常に困難になりました。 - 分離の欠如: プロジェクトは互いに分離されていませんでした。あるプロジェクトの依存関係を変更すると、同じ (グローバル)
GOPATHインストールに依存する他のプロジェクトが不注意で壊れる可能性があります。 - プロジェクトの場所の制約: プロジェクトのソースコードは
GOPATH/src/内にある必要があり、多くの開発者にとって制限的で不自然でした。システム上の任意の場所にリポジトリをクローンし、GOPATHを変更せずにgo buildが機能することを期待することはできませんでした。 - 遅いビルド:
pkg/が役立ったとしても、堅牢な依存関係キャッシュの欠如と頻繁なgo get操作の必要性が、開発を遅らせる可能性があります。
これらの限界は、公式のソリューションが登場する前に、Goコミュニティが dep や Glide などのより良いソリューションを模索するきっかけとなりました。
Go Modules:現代的で堅牢なソリューション (Go 1.11 以降)
Go 1.11 で導入され、Go 1.13 でデフォルトになった Go Modules は、バージョニング、分離、および再現可能なビルドの組み込みのファーストクラスサポートを提供することにより、依存関係管理に革命をもたらしました。
Go Modulesのコアコンセプト
Go Modules は、プロジェクトが依存関係とその特定のバージョンをプロジェクトのルートディレクトリ内で直接宣言できるようにすることで、GOPATH の欠点に対処します。
-
モジュール定義 (
go.mod): Goモジュールの中心はgo.modファイルです。このファイルは、モジュールのパス(そのID)、Goのバージョン要件、および対応する最小必須バージョンを含む直接および間接の依存関係のリストを定義します。module example.com/my-app go 1.22 require ( github.com/gorilla/mux v1.8.0 rsc.io/quote v1.5.2 // rsc.io/samplerの推移的な依存関係 ) -
整合性のためのチェックサム (
go.sum):go.sumファイルは、モジュールの依存関係の暗号化チェックサムを格納します。これにより、他の誰かがあなたのプロジェクトをビルドするときに、go.sumが生成されたときに使用されたものとまったく同じコードを使用し、悪意のある改ざんや偶発的な依存関係の変更を防ぎます。rsc.io/quote v1.5.2 h1:bxz9Fv8DkmA6z5x22z5l+vFz12x... rsc.io/quote v1.5.2/go.mod h1:m5xT+m/0e+Q1X+w0yX... -
モジュールパス: すべてのGoモジュールには、「モジュールパス」があります。これは基本的にそのインポートパスです。GitHubでホストされているモジュールの場合、通常は
github.com/username/repo-nameになります。このパスはgo.modで使用され、go getがモジュールを見つけるためにも使用されます。 -
セマンティックインポートバージョニング: Go Modulesは、セマンティックバージョニング(
MAJOR.MINOR.PATCH)を採用しています。メジャーバージョン (v2、v3 など) の場合、モジュールパス自体に/vN(例:github.com/go-redis/redis/v8) がサフィックスとして付加されます。これにより、同じ依存関係の異なるメジャーバージョンが同じモジュールの依存関係グラフ内で共存できます。v2モジュールをフェッチする新しいユーザーは、パッケージのv2バージョンを自動的に取得します。 -
GO111MODULE環境変数 (移行支援):GOPATHからModulesへの移行中、GO111MODULEはGoツールチェーンの動作を制御しました。auto(デフォルト):$GOPATH/src内では、GOPATHモードを使用します。それ以外の場合は、go.modファイルが存在する場合、モジュールモードを使用します。on:$GOPATH/src内でも、常にモジュールモードを使用します。off: モジュールモードを絶対に使用せず、常にGOPATHモードを使用します。 現在、Go 1.16以降が一般的に使用されており、モジュールモードはほぼ普遍的にデフォルトでonになっており、GO111MODULEは新しいプロジェクトではあまり重要ではありません。
Go Modules を使用する
Go Modules を使用したワークフローは直感的で強力です。
-
新しいモジュールを初期化する: プロジェクトディレクトリに移動し(
GOPATH/srcの外側の任意の場所に配置できます)、次を実行します。mkdir my-go-app cd my-go-app go mod init example.com/my-go-app # モジュールパスこれにより、初期
go.modファイルが作成されます。 -
依存関係を追加する:
.goファイルで新しいパッケージをimportし、go build、go run、またはgo mod tidyを実行すると、Go ツールチェーンは不足している依存関係を自動的に検出し、ダウンロードして、最新の互換性のあるバージョンでgo.modファイルにエントリを追加します。rsc.io/quoteを使用してmain.goを作成してみましょう。// my-go-app/main.go package main import ( "fmt" "rsc.io/quote" // これはダウンロードされ、go.mod に追加されます ) func main() { fmt.Println(quote.Hello()) fmt.Println(quote.Go()) }実行します。
cd my-go-app go run .出力:
Hello, world. Go is a general-purpose language designed with systems programming in mind.go run .(またはgo build) を実行したら、go.modおよびgo.sumを調べます。my-go-app/go.mod:module example.com/my-go-app go 1.22 require rsc.io/quote v1.5.2my-go-app/go.sum(簡潔にするために切り捨てられています):rsc.io/quote v1.5.2 h1:bxz9Fv82... rsc.io/quote v1.5.2/go.mod h1:m5xT+m... rsc.io/sampler v1.3.0 h1:aQ2N... rsc.io/sampler v1.3.0/go.mod h1:t2N...rsc.io/quoteの推移的な依存関係であるため、rsc.io/samplerも追加されていることに注意してください。 -
依存関係を明示的に追加/更新する: 依存関係を特定のバージョンに明示的に追加または更新できます。
go get github.com/gin-gonic/gin@v1.9.1 # 特定のバージョンを追加 go get github.com/gin-gonic/gin@latest # 最新の安定版に更新 go get github.com/gin-gonic/gin@master # masterブランチから取得これらのコマンドは、それに応じて
go.modおよびgo.sumを変更します。 -
未使用の依存関係をクリーンアップする:
go mod tidyこのコマンドは、
go.modおよびgo.sumから未使用の依存関係を削除し、依存関係グラフが最小限で正確であることを保証します。 -
ベンダーリング (オプション): インターネットアクセスが制限された環境の場合、依存関係を「ベンダー」して、プロジェクト内の
vendor/ディレクトリに配置できます。go mod vendor今後のビルドでは、ネットワークからフェッチする代わりに、ベンダーリングされた依存関係を使用します。ただし、
GOFLAGS=-mod=vendorが設定されているか、Goバージョン < 1.14 で暗黙的である必要があります (Go 1.14 以降では、vendorフォルダが存在する場合、ベンダーを暗黙的に使用します)。 -
replaceディレクティブ: ローカル開発またはフォークに役立ちます。ローカルまたはリモートのいずれかで、モジュールの依存関係を別のパスに置き換えることができます。// go.mod module example.com/my-app go 1.22 require ( example.com/my-dep v1.0.0 // 通常はリモートリポジトリを指します ) replace example.com/my-dep v1.0.0 => ../my-dep-local // ローカルバージョンを使用 // または replace example.com/my-dep v1.0.0 => github.com/myuser/my-dep-fork v1.0.0
Go Modulesのメリット
- 再現可能なビルド:
go.modとgo.sumは、依存関係ツリーと暗号化ハッシュを正確に定義し、ビルドがいつでもどこでも同一であることを保証します。 - バージョン管理: 異なるプロジェクト (または同じプロジェクトの推移的な依存関係の異なる部分) が同じパッケージの異なるバージョンを使用できるようにすることで、「依存関係地獄」を解決します。
- プロジェクトの分離: プロジェクトは自己完結型です。ファイルシステムの任意の場所にGoモジュールをクローンでき、
GOPATHを設定したり、プロジェクトをその中に配置したりしなくても、go buildが機能します。 - 簡素化された
go get:go getはバージョンとモジュールを理解し、指定されたものを正確にフェッチするようになりました。 - 依存関係のキャッシュ: 依存関係はグローバルモジュールキャッシュ (通常は
$GOPATH/pkg/mod) にダウンロードされるため、一度だけダウンロードされ、異なるプロジェクト間で再利用されます。 - プロキシサポート (
GOPROXY):GOPROXYを使用すると、モジュールのキャッシュおよび/またはソースとして機能するGoモジュールプロキシサーバーを構成でき、特に企業ネットワークで信頼性とセキュリティが向上します。go.sum検証は整合性を保証します。
進化を理解する:パラダイムシフト
GOPATH から Go Modules への移行は、Go プロジェクトの構造化と管理の方法における根本的な変化を表しています。
- グローバルからローカルへ:
GOPATHはグローバルでモノリシックなワークスペースを課し、すべてのプロジェクトが同じ依存関係のセットを共有していました。Go Modules はこれをローカルでプロジェクト中心のアプローチに移行し、各プロジェクトの依存関係とそのバージョンが明示的に宣言され、分離されます。 - 暗黙的から明示的へ:
GOPATHはディレクトリ構造に基づく暗黙的な検出に依存していました。Go Modules はgo.modとgo.sumを通じて依存関係を明示的にし、明確さと制御を提供します。 - 「とりあえず動く」(場合によっては) から再現可能な安定性へ:
GOPATHは競合する依存関係のないグリーンフィールドプロジェクトには簡単でしたが、それを超えるものにはすぐに頭痛の種になりました。Go Modules は、堅牢なソフトウェア開発に不可欠な安定性と再現性を優先します。
今日、新しい Go プロジェクトはほぼ例外なく Go Modules を使用する必要があります。GOPATH はまだ存在し、Go ツールチェーン自体または非常に古いプロジェクトの目的を果たしていますが、アプリケーションのソースコードまたは依存関係を管理するための推奨される方法ではなくなりました。
結論
GOPATHからGo ModulesへのGoの依存関係管理の進化は、開発者の苦痛に対処し、エコシステムを成熟させるという言語のコミットメントの証です。GOPATHはGoの形成期にその目的を果たし、簡単な規約を確立しました。ただし、Goがより大規模で複雑なシステムで牽引力を獲得するにつれて、その制限が明らかになりました。
Go Modulesは、バージョニング、分離、および再現性の課題をエレガントに解決し、他の言語エコシステムの最新のパッケージマネージャーに匹敵する、強力な組み込みソリューションを提供します。この変革により、信頼性が高く、保守可能で、スケーラブルなアプリケーションを構築するためのGoの魅力が大幅に向上し、Go開発者のエクスペリエンスがこれまで以上にスムーズかつ効率的になりました。この進化を理解することは、すべてのGo開発者にとって非常に重要であり、Goの最新のツールのすべての能力を活用することができます。