少しずつ育てるGo言語のプロジェクト構成
23/9/21追記:この記事を読む前に
ついにGoチームから、プロジェクト構成に関するガイドが公開されました!
本記事を読んでくださることも大変嬉しいですが、ぜひこちらのガイドもご一読ください!
この記事は何
Go言語を書いたことがある方も、興味はあるけど触ったことがない方もこんにちは。
Goに限った話ではないと思いますが、ガリガリコードを書いていて、あるタイミングで気になるのがプロジェクト構成(ここではディレクトリ構成の意図)ではないでしょうか?
それを裏付けるかのように、Go界隈では以下のリポジトリが話題に上がることがあります。Star数すごいですね😇 リポジトリ名から公式感が漂いますが、そういう訳ではないのがミソです。
こちらのリポジトリ冒頭にも記載されていますが、次の点に留意する必要があるでしょう。
これは、Goアプリケーションプロジェクトの基本的なレイアウトです。これは、コアとなるGo開発チームによって定義された公式の標準ではありませんが、Goエコシステムの中で、歴史的に共通しているプロジェクトのレイアウトパターンのセットとなっています。
Goを学ぼうとしている場合や、自分でPoCやおもちゃのプロジェクトを構築しようとしている場合、このプロジェクトレイアウトはやりすぎです。最初は本当にシンプルなものから始めてください(main.goファイルが1つあれば十分です)。
最初は本当にシンプルなものから始めてくださいというのは、本当にその通りだと思います。とは言っても、シンプルに始めて、その次にどうしたらええねん、という疑問が付いてくるのではないでしょうか?
そこで本記事では、シンプルに始めて、徐々にプロジェクトが大きくなって行った時を想定し、その時々でのプロジェクト構成、留意点、Tipsなどを筆者の経験に基づいてまとめてみたいと思います。
最初からベストプラクティスを追い求めてくじけてしまった駆け出しGopherの助けになれたら幸いです。
この記事の主な対象読者
- 規模小さめのプロジェクトを始めるGopher
本記事で扱わないこと
- ベストプラクティスを紹介するものではありません。こういうきっかけがあったらプロジェクト構成を変えていくのかな?という考え方を共有します。
- Go1.18から導入したWorkspaceモードには触れません。興味のある方はぜひ調べてください!
アウトライン
徐々にプロジェクトが大きくなっていくときに、どのようなプロジェクト構成を試してみるのか、次の順を追って例を示してみます。
-
main.go
だけで始める -
main.go
が大きくなったらファイルを分割する - 共通するロジックを使って複数のアプリを作る
- 共通するロジックが大きくなってきたので分割する
- 共通するロジックを、他の人にも使ってもらうために別モジュールに切り出す
- マルチモジュールにする
なお、最後のケースを採用するのは相当な上級者だと思いますので、本記事では紹介程度にとどめます。
プロジェクト構成の実例
以下、今回のために描き下ろしたごふちゃんというストーリーテラーを立てて進めていきたいと思います。
前提条件は以下の通りです。
- Go1.18.2(執筆時点最新)を導入済み
main.go
だけから始める
ごふちゃん「Gopherのみんな、こんにちは。今日は簡単なプログラムをGo言語を使って書いていくよ。」
ごふちゃん「まずは1から10までの数字を順番にターミナルに表示するプログラムを作ってみるね。何の役に立つかは考えちゃダメだよ?」
ごふちゃん「まずはプロジェクト用のディレクトリを作って、っと」
- 古い情報に惑わされがちなポイントその1ですが、基本的にGoのプロジェクトはどこに作っても問題ないです。詳細はGoのプロジェクト構成の基本を参照ください。
ごふちゃん「それから、今のGoはデフォルトでモジュールモードが有効になってるから、何をするにもまずはgo.mod
を作らないとね?今回はgithub.com/tenkoh/go-counter
ってモジュール名にしてみるね。」
ごふちゃん「今までのコマンドをまとめるとこんな感じだよ。」
mkdir go-counter
cd go-counter
go mod init github.com/tenkoh/go-counter
- モジュールをGithub等で公開する前提であれば、モジュール名は上記のようにリポジトリのURL形式が推奨されています。(非公開であれば例えば末尾要素の
go-counter
だけでも問題ないです) - ここで覚えておく点として、Goにおけるリポジトリ>モジュール>パッケージという関係があります。1リポジトリには複数のモジュールを含められますし、1モジュールには複数のパッケージを含めることができます。具体的なイメージはこの後説明します。
ごふちゃん「準備ができたから、プログラムを書き始めるね。実行ファイルをビルドするのが目的だし、シンプルな機能だから、main.go
を作っておけば良さそうかな。」
ごふちゃん「ということでプロジェクト構成はこんな感じになったよ。」
go-counter/
|- go.mod
|- main.go
ごふちゃん「main.go
の中身で、ポイントになるところだけ覗いてみるね。」
package main
func main() {
// ...
}
ごふちゃん「後で実行ファイルとしてビルドするために、package
名はmain
にして、main()
関数を持つことが必要だよ。」
ごふちゃん「プログラムを書き終わってgo build
すると実行ファイルが出来上がるよ。オプションで出力名を指定しなかったら、モジュール名末尾要素と同じgo-counter
という名前になるよ。」
ごふちゃん「簡単だけど、最初の例はこれでおしまいね。お疲れ様!」
main.go
が大きくなったらファイルを分割する
ごふちゃん「じゃあ次のステップにいくね。」
ごふちゃん「機能を実装したら、なんだかんだmain.go
が大きくなっちゃった。見通しが悪いから、ファイルを分割してみるね。こんな感じにするよ。」
go-counter/
|- go.mod
|- main.go
|- count.go
ごふちゃん「気を付けるポイントはパッケージ名ぐらいかなぁ?同じディレクトリ中のファイルは同じパッケージじゃないとダメだから、count.go
はこんな感じになるよ。」
package main
func Count(start, end int) {
// ...
}
ごふちゃん「パッケージ名をmain
にしてあるよね。ここに気をつければOKだよ。」
ごふちゃん「簡単な実行ファイルをビルドする目的なら、これぐらいのシンプルな構成でもいいんじゃないかな?」
共通するロジックを使って複数のアプリを作る
ごふちゃん「さっき作った実行ファイルとは別に、10から1までカウントダウンする実行ファイルも作りたくなっちゃった。前のステップで作ったcount.go
を使い回してあげれば良さそうだけど、悩ましいのはプロジェクト構成だねぇ…。例えばこんなのはどうかな?」
go-counter/
|- cmd/
| |- countup/
| | |- main.go
| |- countdown/
| |- main.go
|- go.mod
|- count.go
ごふちゃん「共通して使うロジックはリポジトリ直下において、実行ファイルはcmd
ディレクトリ以下に、それぞれディレクトリを作っておくのが良く見るパターンかなぁ。」
ごふちゃん「ただディレクトリ構成を変えるだけじゃなくて、パッケージ名なんかを少し修正する必要があるね。」
package counter
func Count(start, end int) {
// ...
}
ごふちゃん「リポジトリ直下のファイル一式をここではcounter
という名前のパッケージで定義したよ。モジュール名の末尾要素はgo-counter
だけど、リポジトリ直下のパッケージ名はcounter
で別になっちゃってるね。(正直ルールを正確に分かってないんだけど)、go-
ぐらいの接頭詞違いなら不都合なく開発・配布ができるよ。」
- 補足ですが、以下のような関係がよく見られるのではないでしょうか?
- リポジトリ名 = モジュール名の末尾要素(今回だと
go-counter
) - リポジトリ直下のパッケージ名 = モジュール名の末尾要素またはその接頭詞の省略(今回だと
counter
)
- リポジトリ名 = モジュール名の末尾要素(今回だと
ごふちゃん「じゃあ./cmd/countup/main.go
からpackage counter
を利用するために、少し変更を加えるね。」
package main
import "github.com/tenkoh/go-counter"
func main() {
counter.Count(1, 10)
// ...
}
ごふちゃん「こんな感じでリポジトリ直下においた共通メソッドを呼び出すことができるよ。」
ごふちゃん「ちなみに、この状態で./cmd/countup/main.go
をビルドしてあげると、オプションを指定しなければcountup
という名前の実行ファイルが出来上がるよ。ディレクトリの名前と同じだね。」
ごふちゃん「なんだか、だいぶプロジェクトらしさが出てきたね。このステップはここでおしまいだよ。」
共通するロジックが大きくなってきたので分割する
ごふちゃん「いろんな機能を追加してみたんだけど、counter
パッケージが大きくなってきちゃった。表示形式をいろいろ変える本質的じゃないロジックがいっぱいあるから、それを別のパッケージに分けて呼び出せるようにしてみるね。」
go-counter/
|- cmd/
| |- countup/
| | |- main.go
| |- countdown/
| |- main.go
|- go.mod
|- count.go
|- style/
|- style.go
|- bold.go
|- italic.go
ごふちゃん「./style/bold.go
の要点はこんな感じかな。」
package style
func Bold() *CounterStyle {
// ..
}
ごふちゃん「別のパッケージ名で定義してあげてるね。これをリポジトリ直下のcounter
パッケージから使おうと思うと、こんな感じになるよ。」
package counter
import "github.com/tenkoh/go-counter/style"
func BoldCount(start, end int) {
bold := style.Bold()
// ..
}
ごふちゃん「なんとなく雰囲気が伝わってくるかな?」
ごふちゃん「このステップで気を付けるのは、パッケージ間の循環参照が起きないようにすることだね。私も経験あるけど、行き当たりばったりでパッケージを分割すると循環参照しちゃうから、ご利用は計画的に、だね…。」
共通するロジックを、他の人にも使ってもらうために別モジュールに切り出す
ごふちゃん「なんだかcounter
パッケージ、いろいろ流用が効きそうな気がしてきたから、モジュールとして配布してみようかな?今までのステップで進めてきたら、cmd
ディレクトリを除けばほぼ配布できる状態になるね。(別にcmd
もそのままでも良いんだけどね)」
ごふちゃん「そういうわけでこんな構成でモジュールとして配布するよ」
go-counter/
|- go.mod
|- count.go
|- style/
|- style.go
|- bold.go
|- italic.go
ごふちゃん「実際はここにドキュメントとかライセンスとか、配布にあたり必要なリソースを追加することになるね。」
マルチモジュールにする
ごふちゃん「ここまではリポジトリ内にgo.mod
が一つだけある構成だったけど、リポジトリ内に複数のgo.mod
を置くこともできるよ。私はそこまで大規模な開発の必要性に直面したことがないので、例えば下記を参考にしてみてね。」
Sourcing multiple modules in a single repository
おわりに
いかがでしたでしょうか。やりたいこと、コードの規模感に応じて、シンプルに始めて、徐々にプロジェクト構成を育てていく感覚が少しでも伝われば幸いです。
ノリで召喚したごふちゃん、説明ありがとう。
Discussion