🍙

少しずつ育てるGo言語のプロジェクト構成

2022/05/26に公開

23/9/21追記:この記事を読む前に

ついにGoチームから、プロジェクト構成に関するガイドが公開されました!
本記事を読んでくださることも大変嬉しいですが、ぜひこちらのガイドもご一読ください!

https://go.dev/doc/modules/layout

この記事は何

Go言語を書いたことがある方も、興味はあるけど触ったことがない方もこんにちは。
Goに限った話ではないと思いますが、ガリガリコードを書いていて、あるタイミングで気になるのがプロジェクト構成(ここではディレクトリ構成の意図)ではないでしょうか?

それを裏付けるかのように、Go界隈では以下のリポジトリが話題に上がることがあります。Star数すごいですね😇 リポジトリ名から公式感が漂いますが、そういう訳ではないのがミソです。

https://github.com/golang-standards/project-layout

こちらのリポジトリ冒頭にも記載されていますが、次の点に留意する必要があるでしょう。

これは、Goアプリケーションプロジェクトの基本的なレイアウトです。これは、コアとなるGo開発チームによって定義された公式の標準ではありませんが、Goエコシステムの中で、歴史的に共通しているプロジェクトのレイアウトパターンのセットとなっています。
Goを学ぼうとしている場合や、自分でPoCやおもちゃのプロジェクトを構築しようとしている場合、このプロジェクトレイアウトはやりすぎです。最初は本当にシンプルなものから始めてください(main.goファイルが1つあれば十分です)。

最初は本当にシンプルなものから始めてくださいというのは、本当にその通りだと思います。とは言っても、シンプルに始めて、その次にどうしたらええねん、という疑問が付いてくるのではないでしょうか?

そこで本記事では、シンプルに始めて、徐々にプロジェクトが大きくなって行った時を想定し、その時々でのプロジェクト構成、留意点、Tipsなどを筆者の経験に基づいてまとめてみたいと思います。
最初からベストプラクティスを追い求めてくじけてしまった駆け出しGopherの助けになれたら幸いです。

この記事の主な対象読者

  • 規模小さめのプロジェクトを始めるGopher

本記事で扱わないこと

  • ベストプラクティスを紹介するものではありません。こういうきっかけがあったらプロジェクト構成を変えていくのかな?という考え方を共有します。
  • Go1.18から導入したWorkspaceモードには触れません。興味のある方はぜひ調べてください!

アウトライン

徐々にプロジェクトが大きくなっていくときに、どのようなプロジェクト構成を試してみるのか、次の順を追って例を示してみます。

  1. main.goだけで始める
  2. main.goが大きくなったらファイルを分割する
  3. 共通するロジックを使って複数のアプリを作る
  4. 共通するロジックが大きくなってきたので分割する
  5. 共通するロジックを、他の人にも使ってもらうために別モジュールに切り出す
  6. マルチモジュールにする

なお、最後のケースを採用するのは相当な上級者だと思いますので、本記事では紹介程度にとどめます。

プロジェクト構成の実例

以下、今回のために描き下ろしたごふちゃんというストーリーテラーを立てて進めていきたいと思います。

gofu.png

前提条件は以下の通りです。

  • 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の中身で、ポイントになるところだけ覗いてみるね。」

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はこんな感じになるよ。」

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ディレクトリ以下に、それぞれディレクトリを作っておくのが良く見るパターンかなぁ。」

ごふちゃん「ただディレクトリ構成を変えるだけじゃなくて、パッケージ名なんかを少し修正する必要があるね。」

count.go
package counter

func Count(start, end int) {
  // ...
}

ごふちゃん「リポジトリ直下のファイル一式をここではcounterという名前のパッケージで定義したよ。モジュール名の末尾要素はgo-counterだけど、リポジトリ直下のパッケージ名はcounterで別になっちゃってるね。(正直ルールを正確に分かってないんだけど)、go-ぐらいの接頭詞違いなら不都合なく開発・配布ができるよ。」

  • 補足ですが、以下のような関係がよく見られるのではないでしょうか?
    • リポジトリ名 = モジュール名の末尾要素(今回だとgo-counter)
    • リポジトリ直下のパッケージ名 = モジュール名の末尾要素またはその接頭詞の省略(今回だとcounter

ごふちゃん「じゃあ./cmd/countup/main.goからpackage counterを利用するために、少し変更を加えるね。」

cmd/countup/main.go
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の要点はこんな感じかな。」

./style/bold.go
package style

func Bold() *CounterStyle {
  // ..
}

ごふちゃん「別のパッケージ名で定義してあげてるね。これをリポジトリ直下のcounterパッケージから使おうと思うと、こんな感じになるよ。」

count.go
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

おわりに

いかがでしたでしょうか。やりたいこと、コードの規模感に応じて、シンプルに始めて、徐々にプロジェクト構成を育てていく感覚が少しでも伝われば幸いです。

ノリで召喚したごふちゃん、説明ありがとう。

その他、関連する公式ドキュメント

GitHubで編集を提案

Discussion