Goの依存関係周りについて整理してみた
はじめに
こちらはe-dash advent calendar 2024の13日目の記事です。
実は、本日は13日の金曜日です。クリスマスがイエス・キリストの降誕をお祝いする日であるのと並び、「13日の金曜日」もまたキリストに関係があるとか。気になる方は検索してみてください。
こんにちは。e-dashのプロダクト開発部所属のkakenyanです。
私は8月にe-dashに転職してきて、そこから初めてGo言語での開発に携わっています。
最近は同僚さんから教えていただいた、ニトリの着る毛布に生活を支配されかけています。
先日、e-dashアプリケーションのGo言語のバージョンをアップするタスクを実施しました。
その際に調べてるうち、moduleやworkspaceなどの話が出てきて、気付けば色々あって良く分からない…という状況になっていました。ですので、今回はそれらの全体感が把握できるように整理したいと思います。
この記事の対象読者は、Goのフォルダ構成・依存関係に関して改めて基本的なところから理解したい方を想定しております。
想定するフォルダ構成
今回話を進めていく中で、フォルダ構成はざっくり以下のようなモノレポを想像していただければと思います。
.
└── project-root/
├── moduleA/
│ ├── main.go
│ ├── go.mod
│ ├── go.sum
│ ├── packageA/
│ │ ├── func1.go
│ │ └── func2.go
│ └── packageB/
│ │ └─ ...
│ └─ ...
│
├── moduleB/
│ ├── main.go
│ ├── go.mod
│ ├── go.sum
│ ├── packageA/
│ │ └── ...
│ └── ...
│
├── ...(other modules)
│
├── go.work
└── go.work.sum
-
project-root
フォルダ下に複数のモジュールを持つ、マルチモジュールな状態である- それぞれのモジュールのフォルダ下に、
go.mod
が存在する
- それぞれのモジュールのフォルダ下に、
- 各モジュールの下で、複数のパッケージが作成されている
-
project-root
フォルダ直下にgo.work
を持ち、ワークスペースというツールを用いている
パッケージ・モジュールについて
ソースファイルをまとめていく単位として、小さい方から順に見ていきたいと思います。
パッケージ
いわゆるフォルダに該当します。Goファイルは全てのソースコードが何かしらのパッケージに属することになります。
インポートを可能とするための概念であり、再利用可能なまとまりとして分割する基本単位になります。
- 慣習として、パッケージ名とフォルダ名は同一にすることが推奨されている
- 同じフォルダ内にあるGoファイルの先頭には、
package <パッケージ名>
が統一されて記載される - main関数を持つGoファイルは
package main
に属していなければならない
- 同じフォルダ内にあるGoファイルの先頭には、
- 同じパッケージ内のファイルの変数や関数は、import不要で共有される。
- 同じパッケージに属するソースコードは、同時にコンパイルされる
例えばpackageA/func1.go
内で定義された関数は、packageA/func2.go
の中でimportせずに利用することができます。一方project-root/moduleA/main.go
においてpackageAやpackageBで定義された関数を呼び出したい場合には、それぞれのimportが必要になります。
モジュール
前述のパッケージを1つ以上まとめたものが、モジュールという単位になります。コードの依存関係やバージョンは、このモジュールという単位で管理します。
依存関係を追加することによって、他の開発者が作成・公開しているモジュールを使用することもできますし、自分が作成したモジュールを外部に公開することもできます。
go.mod
ファイルについての大まかな内容は以下の通りです。
- モジュールを初期化すると
go.mod
というファイルが生成されて、ここにモジュール名や依存関係が記載される -
go mod init <モジュール名>
のコマンドを実行することによってgo.mod
が作成されて、そのフォルダをGoモジュールとして認識させることができる -
go get <利用する外部パッケージ名>
のコマンドを実行すると、go.mod
にそのパッケージの依存関係を記載する-
-u
オプションによってバージョンの更新もできる
-
-
go mod tidy
のコマンドを実行すると、go.mod
の中身を整理することができる- コード中でimportされているものは反映され、
go.mod
には記述があるがコード中でimportされていなかったものは削除される
- コード中でimportされているものは反映され、
-
go mod tidy
の実行時に更新されるgo.sum
には、依存先モジュールのハッシュが記録される。これらは依存先モジュールが改竄されていないかをチェックするために用いられる
go.mod
には依存先モジュールの情報だけでなく、自身のモジュールで利用するGoのバージョンが記載されています。ここに書かれるバージョンでのみビルドが可能という訳ではなく、そのバージョンまでの機能が利用できるという形になります。
実際に私が実施したバージョンアップについても、各go.mod
のバージョンの記述を更新し、正常にビルドが可能かを確認するということが基本的な内容でした。
モジュールごとに記載されているGoのバージョンの更新と、パッケージの依存関係の整理とを混同してしまったせいで、全体感が迷子になってしまったんですけどね...
モジュールのバージョン付け及び公開については今回は細かく言及しませんので、もっと詳しく知りたいは以下をご覧ください!
ワークスペースについて
Go 1.18から導入されたGo Workspaceは、複数のモジュールを一度に扱うための機能です。これは、開発中の複数のモジュールを1つの作業環境として管理できるようにするもので、相互に依存する複数のモジュールを同時に開発する際に便利です。
複数モジュールの親フォルダとなる(例ではproject-root
)の階層でgo work init
というコマンドを実行すると、go.work
ファイルが作成されます。
続いてワークスペースを利用するモジュールを追加するためにgo work use ./moduleA ./moduleB
というコマンドを実行すると、go.work
ファイルは以下のような状態になります。
go 1.23.3
use (
./moduleA
./moduleB
)
go.mod
と同じくワークスペースで使用するGoのバージョンが指定されていると同時に、ワークスペースで管理するモジュールのパスが指定されています。
例えばモジュールAのコードの中で、モジュールBに定義された関数を利用しているとします。モジュールBに更新が入った場合、本来はタグ打ち→リリース→go get -u <モジュールBのパス&対象バージョン>
という作業が必要ですが、毎回リリースするのは面倒ですよね。
ここでワークスペースの出番です。上記のように同じワークスペースに含まれている場合、このリリースの作業を行うことなく、モジュールBの変更を反映した状態でモジュールAを実行することができます。
ローカルで開発中のモジュールBを参照する手段として、モジュールA内のgo.mod
の中でreplace
ディレクティブの指定を行う手段もあります。
module github.com/xxx/moduleA
go 1.23.3
require (
github.com/xxx/moduleB v0.0.0
...(略)
)
// replaceディレクティブによって、参照先をローカルのフォルダに変更
replace github.com/xxx/moduleB => ../moduleB
どちらの方法でも可能なのですが、ワークスペース機能の方が比較的手間も少なく利用できるかなとは思います。
いずれにしても、ローカル環境での開発時に依存関係や参照を容易にしてくれる機能であり、そのままの状態でリモートリポジトリにpushしないよう注意する必要があります。
まとめ
本記事では、Goのパッケージ・モジュール・ワークスペースの基本的な役割と、バージョンや依存関係の管理について簡単に整理してみました。
まだまだ詳細を把握できていない部分も多いですが、Go言語のプロジェクトがどのような構造になっていて、それぞれの単位がどんな役割を持っているか、ということへの理解が深められたと感じています。
内容自体に目新しいものはないと思いますが、私と同じ初学者の方や、あれって何だったっけ?とふと感じた方の参考になれば良いなと思っております。
ちょうどAdvent Calendarも折り返し地点ですね。明日の記事も楽しみにお待ちください!🎄
参考
参考にさせていただいた公式ドキュメントや記事等について、以下にまとめておきます。
Discussion