「Go プロジェクト構成」で検索すると、2 クリックもしないうちに golang-standards/project-layout にたどり着きます。5 万以上のスターを集めた GitHub リポジトリで、ディレクトリツリーには cmd/、pkg/、internal/、api/、build/ がずらりと並んでいます。いかにも権威がありそうに見えるため、多くの初心者は、ファイルが 5 つしかないプログラムにこの構成を丸ごと取り入れてしまいます。
しかし、これは公式の標準ではなく、Go チームも公の場でそう述べています。公式のガイダンスはもっとシンプルです。最初はフラットに始め、コードが必要としたときだけフォルダを追加します。以下では、天気 CLI と書店 API を例に、そのガイダンスをフォルダ 1 つずつ見ていきます。
Go の標準的なプロジェクト構成とは?
必ず従うべき唯一のディレクトリツリーはありません。Go は src/ も cmd/ も必須にしておらず、モジュールのルートにある go.mod ファイル以外には、どんなフォルダも必要ありません。公式ガイド「Organizing a Go module」は、構成をプロジェクトの規模と結びつけています。そこでは 4 つの構成が、前のものから 1 段階ずつ大きくなる順に示されています。基本的なパッケージ、基本的なコマンド、補助的なパッケージを抱えるまで成長したパッケージ、そして複数のバイナリを持つ「サーバー」プロジェクトです(出典)。
つまり役に立つ問いは、自分のプロジェクトに今どれだけの構成が必要か、であり、ほとんどのプログラムではその答えは「ごくわずか」です。この記事の残りでは、4 つの構成を最もシンプルなものから順に見ていきます。自分のプロジェクトに合う構成を見つけたら、そこで止めてください。
フラットに始める:パッケージ 1 つが Go らしい構成
公式ドキュメントの最初の 2 つの構成は、単一のパッケージと単一のコマンドで、どちらもサブディレクトリを持たずにモジュールのルートに直接置かれます。都市名を読み取って天気を表示する小さなコマンドラインツールなら、プロジェクトはこれですべてです。
example.texttextweather/ ├── go.mod ├── main.go ├── weather.go └── weather_test.go
go.mod はモジュールパス(たとえば module github.com/you/weather)を宣言します。main.go には package main とエントリーポイントを置きます。weather.go にはロジックを、同じパッケージ内に置きます。weather_test.go には、対象のコードのすぐ隣に置いたテストを書きます。そこが、Go がテストの置き場所として想定している位置です。このままの形でリリースできます。公式ドキュメントはこれを「基本的なコマンド」と呼んでおり、最初の 1 年に書くほとんどのツールにとって適切な形です。
この後に出てくる構成上の判断のほとんどは、1 つのルールで説明できます。ディレクトリはパッケージである、というルールです。ディレクトリ内のすべてのファイルは同じパッケージを宣言し、パッケージ名はインポートパスの最後の要素になります。weather/store/ ディレクトリを追加すると、その中のファイルはすべて package store と宣言します。インポートするときは github.com/you/weather/store と書き、呼び出すときは store.Get と書きます。
まだフォルダは追加しないでください。関数が 1 つだけの utils/ ディレクトリや、struct が 1 つだけの models/ ディレクトリは、インポートパスとパッケージ境界を増やすだけで、見返りは何もありません。フラットであることが実際に問題を起こし始めるまでは、フラットなままにしておきましょう。
internal/ ディレクトリはいつ追加すべきか?
構成を追加する最初の正当な理由は、コードを非公開にすることです。Go の識別子の可視性は 2 段階です。大文字で始まる名前はエクスポートされ、小文字で始まる名前はそのパッケージ内でしか使えません。1 つのパッケージの中ならこれで十分です。しかし、モジュールが複数のパッケージを持つようになると、モジュールの内側のパッケージならどこからでも使えて、外側からはインポートできないコードが欲しくなることがよくあります。それを実現するのが internal/ で、コンパイラがこの制限を強制します。
これは命名上の慣習ではなく、言語の一部です。internal/ という名前のディレクトリ配下にあるパッケージは、その internal/ ディレクトリの親をルートとするコードからしかインポートできず、別のモジュールからインポートするとビルドが失敗します。公式ドキュメントは、パッケージを「できる限り」internal に置くことを推奨しています。外部のコードはそれらをインポートできないので、それらに依存するようになることもありません。公開するつもりのなかった API をサポートし続ける羽目になることもありません(出典)。
先ほどの天気 CLI が、書店向けの小さな HTTP API になったとしましょう。リクエストの認証とデータアクセス層が加わりましたが、どちらも公開 API に含めるべきものではありません。
example.texttextbookstore/ ├── go.mod ├── main.go └── internal/ ├── auth/ │ ├── auth.go │ └── auth_test.go └── store/ ├── store.go └── store_test.go
main.go は bookstore モジュールの中にあるので、github.com/you/bookstore/internal/auth と github.com/you/bookstore/internal/store を自由にインポートできます。他のモジュールのコードはインポートできません。誰かがあなたのモジュールを go get して、これらのパッケージをインポートしようとしても、コンパイラがそのインポートを拒否します。モジュールの外にいる誰も auth や store に依存できないので、関数名の変更、シグネチャの変更、構成の組み直しをいつでも好きなときに行えます。それで他の誰かのコードが壊れることはありません。
cmd/ ディレクトリはいつ追加すべきか?
cmd/ は、複数のバイナリをビルドするリポジトリや、バイナリとインポート可能なライブラリの両方を提供するリポジトリ向けです。バイナリが 1 つだけなら cmd/ は必要なく、ルートに main.go を置くほうがすっきりします。
cmd/ の下では、バイナリごとに専用のサブディレクトリを用意し、そのディレクトリ名がプログラムの名前になります。公式の「サーバープロジェクト」構成もこの形です。各コマンドは cmd/<name>/ に置かれ、internal/ のパッケージを組み合わせる小さな main.go を持ちます(出典)。書店アプリが、API サーバーとデータベースのマイグレーションツールの両方を提供するようになったとしましょう。
example.texttextbookstore/ ├── go.mod ├── cmd/ │ ├── api/ │ │ └── main.go │ └── migrate/ │ └── main.go └── internal/ ├── auth/ └── store/
go build ./cmd/api を実行すると api という名前のバイナリが、go build ./cmd/migrate を実行すると migrate が生成されます。どちらの main.go も薄いままで、実際の処理は internal/ からインポートします。
この段階的な流れ全体を 1 つの表にまとめました。将来こうなるだろうと思い描いているプロジェクトではなく、今のプロジェクトに合う行を選んでください。
| プロジェクトの形 | 推奨される構成 | 理由 |
|---|---|---|
| 小さなツールやスクリプト | フラット:ルートに main.go とロジックのファイルを置く | パッケージ 1 つで Go らしく、完結している |
| 再利用する内部コードを持つバイナリ 1 つ | 非公開パッケージ用に internal/ を追加 | コンパイラが保証する非公開性、時期尚早な API を作らない |
| 2 つ以上のバイナリ | バイナリごとに cmd/<name>/ を追加 | 各 main が専用のディレクトリを持つ |
| 複数サービスのリポジトリ(バイナリと共有コード) | cmd/ と internal/、ドメインごとに分割 | 公式の「サーバープロジェクト」構成 |
golang-standards/project-layout にまつわる誤解
golang-standards/project-layout リポジトリは、GitHub の Organization 名に「standards」を含み、5 万以上のスターを集め、api/、build/、configs/、deployments/、pkg/、test/ からなる大きなツリーを持っています。Go チームのお墨付きがあると思い込みやすいのですが、実際にはありません。現在はリポジトリの README に、公式の標準ではないという注意書きが載っています。そして、この構成に異を唱えたのは Go 自身の指導者たちでした。
issue #117 で、Go のテックリードである Russ Cox 氏は、これは「標準的な Go プロジェクト構成ではない」と書きました。さらに、ほとんどの Go リポジトリは「はるかにシンプル」で、pkg/ ディレクトリをまったく使っていないとも述べています。このスレッドは Hacker News のトップページに載り、経験豊富な Go 開発者の多くが Cox 氏に同意しました。この構成は、複数のチームで共有する大規模なモノレポなら理にかなう場合もありますが、普通のプロジェクトを始めるときのテンプレートとしては害になります。小さなプログラムにこれを持ち込むと、5 つのファイルが、ほぼ空の 8 つのフォルダの下に埋もれてしまいます。
ここからいくつかのルールが導かれます。これらは、Eli Bendersky 氏や Alex Edwards 氏といった Go の実践者がよく勧めていることとも一致します。
src/ディレクトリは使わないでください。これは Java や古い Node プロジェクトから来た習慣です。Go のコードはモジュールのルートに置き、公式の構成のどれにもsrc/はありません。- 中に入れるコードがないうちからフォルダを入れ子にしないでください。空の
api/、build/、deployments/フォルダは何の役にも立ちません。フォルダは、中に入れるものができた日に作りましょう。 - パッケージには役割ではなく、提供するものに合わせて名前を付けてください。
utils、common、helpersといった名前のパッケージは、読み手に何も伝えず、何でも放り込むガラクタ置き場になります。store、auth、weather、retry、slugifyのように名付けましょう。パッケージ名についての Go の公式のアドバイスは、短く明確にし、呼び出し側で読みやすくすることです。そのためutils.GetFromStoreよりstore.Getのほうが優れています(出典)。
フォルダではなくコードに現れる間違いについては、Go でよくある間違いと回避方法のガイドをご覧ください。
初心者のための Go プロジェクト構成の経験則
ここまでの流れは、プロジェクトについてのいくつかの質問にまとめられます。上から順に答えていき、答えが求めるときだけディレクトリを追加してください。
flowchart TD A["New Go project"] --> B["Is it one package<br/>of related code?"] B -->|Yes| C["Keep it flat:<br/>main.go + logic in the root"] B -->|"Grew past that"| D["Need to hide code<br/>from outside importers?"] D -->|Yes| E["Add internal/<br/>put private packages there"] E --> F["More than one<br/>binary to build?"] F -->|No| G["Done. main.go in root,<br/>logic in internal/"] F -->|Yes| H["Add cmd/name/<br/>one dir per binary"] H --> I["Many services sharing code?"] I -->|Yes| J["Server layout:<br/>cmd/ + internal/ by domain"]
どの段階も、具体的な必要に応えるものです。パッケージ境界、2 つ目のバイナリ、非公開にしておきたいコードのいずれかです。そのどれも挙げられないなら、おそらくそのフォルダは必要ありません。フラットに始め、コンパイラに非公開性を強制させたくなったら internal/ を追加し、2 つ目のバイナリができたら cmd/ を追加します。本格的なサーバー構成に移るのは、リポジトリが実際に複数のサービスを抱えるようになったときだけです。
よくある質問
Go に公式のプロジェクト構成はありますか?
いいえ。必須とされる唯一のフォルダ構成はありません。公式ドキュメント「Organizing a Go module」は、1 つの固定された構成ではなく、プロジェクトの規模に合わせて成長する構成を説明しています。フラットなパッケージから internal/、次に cmd/、そして本格的なサーバー構成へと進みます。自分のプロジェクトに合う、最も小さな構成を使ってください(出典)。
pkg/ ディレクトリは使うべきですか?
通常は使いません。Go のテックリードである Russ Cox 氏は、ほとんどの Go リポジトリは pkg/ ディレクトリを使っておらず、「はるかにシンプルな傾向がある」と指摘しています。非公開のコードは、コンパイラが非公開性を強制してくれる internal/ に置きましょう。モジュールのルートでパッケージを公開するのは、公開 API としてサポートする予定があるときだけにします(出典)。
internal/ と cmd/ にはそれぞれ何を置きますか?
internal/ には、自分のモジュールだけが使えるインポート可能なライブラリコードを置き、その制限はコンパイラが強制します。cmd/<name>/ には、ビルドするバイナリごとに薄い main パッケージを置き、そこで internal/ のコードを組み合わせます。ロジックは internal/ に、エントリーポイントは cmd/ に置きます(出典)。
実際の Go プロジェクトを 1 つのファイルやパッケージだけで構成してもよいですか?
はい。公式の構成ガイドの最初の例は、モジュールのルートに置いた単一のパッケージです。本番環境で使われているツールにも、その形でリリースされているものはたくさんあります。go.mod、main.go、そしてテストを備えた 1 つのパッケージは、Go らしく完結した構成です。コードがその構成に収まらなくなったら、構成を追加してください(出典)。
Go のプロジェクトは Java や Node のプロジェクトとどう違いますか?
Go には src/ ディレクトリがありません。コードはモジュールのルートに置き、ディレクトリ 1 つが 1 つのパッケージになり、パッケージ名はパスの最後の要素になります。Maven やバンドラーのように、ビルドツールがツリーを押し付けることもありません。そのため Go の構成はフラットに保たれ、必要になったときだけ大きくなります(出典)。
次のステップ
プロジェクト構成は、Go の中では比較的やさしい部分です。プログラムはフラットなままリリースし、internal/ や cmd/ はコードが必要としたときに追加します。なぜそのフォルダがあるのか説明できないなら、おそらく削除してかまいません。
感覚をつかむ一番の方法は、CLI と HTTP サービスのような実際のプログラムをいくつか作り、進めながら構成を育てていくことです。LevelUpGo の Clean Go Code トラックでは、ブラウザ上で自分で書いて実行する演習を通して、Go らしいパッケージ設計とプロジェクト構成を学べます。Go Fundamentals では、最初の go.mod とツールチェーンから始められます。インタラクティブなロードマップの全体もご覧いただけます。
