Menu

Go言語のプロジェクト構成:cmd、internal、パッケージの分け方

Goのプロジェクトの構成方法を解説します。フラットに始め、理由があるときにパッケージに分け、複数のバイナリにはcmd/を、他から誰もインポートできないコードにはinternal/を使い、パッケージにうまく名前を付け、テストはコードの隣に置きます。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

1つのファイルから始める

Goのプログラムはまるごと1つの main.go に収められ、小さなツールならそうするべきです。次の完全なプログラムは、入力をパースし、処理をして、レポートを表示するまでを1つのパッケージで行います。

大きくなってきたら、同じディレクトリ、同じパッケージの中で 複数のファイル(parse.goreport.gomain.go)に分けます。1つのパッケージのファイルはすべての識別子を共有するので、何かを公開したりインポートしたりする必要はありません。数千行のプロジェクトなら、たいていはそれだけの構成で足ります。

複数のファイルからなる package main は、go run main.go ではなく go run . のようにパッケージとして実行しなければならないことを忘れないでください。そうしないとファイルが単独でコンパイルされ、他のファイルで定義した名前について undefined エラーが出ます。

決められた構成はない

Goが求めるディレクトリ構成は「1つのディレクトリに1つのパッケージ」以外にありません。公式のガイダンスはgo.devのOrganizing a Go moduleというページで、いくつかの形を説明するもので、ルールではありません。

GitHubの golang-standards/project-layout リポジトリは広く真似され、標準だと広く誤解されています。これは大きなプロジェクトの慣習を集めたコミュニティのもので、当時Goの技術リーダーだったRuss Coxが、Goの標準ではないと述べるissueをそこに立てています。そのディレクトリの大半(pkg/api/build/deployments/)が意味を持つのは大きなコードベースだけです。新しいプロジェクトに骨組み全体をコピーすると、何の利点もなく空のフォルダと深いインポートパスができるだけです。

ツールが強制するルールは短いものです。

  • 1つのディレクトリが1つのパッケージ。その中のすべての .go ファイル(_test 接尾辞を使う _test.go ファイルを除く)はパッケージ名を共有する。
  • インポートパスは、モジュールパスにディレクトリのパスを付けたもの。
  • internal という名前のディレクトリは、その中身をインポートできる相手を制限する。
  • testdata という名前のディレクトリと、._ で始まるディレクトリはgoツールに無視される。

よくある形

単一のコマンド

todo/
├── go.mod        module github.com/you/todo
├── main.go
├── parse.go
├── report.go
└── parse_test.go

フラットで、すべてが package main です。go install github.com/you/todo@latest が動き、バイナリの名前は todo になります。

ライブラリ

slug/
├── go.mod        module github.com/you/slug
├── slug.go       package slug
├── slug_test.go
├── example_test.go
└── internal/
    └── table/    helpers the public package uses, hidden from users

パッケージがモジュールのルートにあるので、利用者は github.com/you/slug をインポートして slug.Make(...) を呼びます。公開APIとしてサポートしたくないものは、すべて internal/ の下に置きます。

サービスや複数のバイナリ

shop/
├── go.mod                module github.com/you/shop
├── cmd/
│   ├── shop-api/
│   │   └── main.go       package main: flags, config, wiring
│   └── shop-worker/
│       └── main.go
├── internal/
│   ├── order/            package order: domain types and logic
│   │   ├── order.go
│   │   └── order_test.go
│   ├── store/            package store: database access
│   └── httpapi/          package httpapi: handlers, routing
├── migrations/
└── README.md

cmd/<name>/main.go はバイナリごとに1つのディレクトリを持ち、ディレクトリ名がバイナリ名になります(go build ./cmd/shop-apishop-api を生成します)。各 main は薄く保ちます。設定を読み、依存関係を組み立て、サーバーを開始するだけです。本当のコードは internal/ の下のパッケージにあり、両方のバイナリから使えて、他のどのモジュールからも使えません。

これは、ほとんどのGoのサービスが最終的に落ち着く構成です。初日からではなく、2つ目のバイナリができたときや、パッケージを分ける本当の理由ができたときに使いましょう。

internal/はgoコマンドが強制する

internal/ の下のパッケージは、internal の親をルートとするツリーの中のコードからしかインポートできません。別のモジュールからだと、ビルドが失敗します。

package example.com/b
	main.go:6:2: use of internal package example.com/a/internal/secret not allowed

そのため、internal/ はAPIの表面を小さく保つための道具になります。すべての呼び出し側が自分のリポジトリの中にあるとわかっているので、そこのコードは自由に変更できます。アプリケーションなら、ほぼすべてを internal/ に置くのも妥当です。ライブラリなら、安定を約束するものとしないものを分けられます。

internal はどの深さでも機能します。shop/internal/ordershop/ 全体から見えますが、shop/internal/order/internal/pricingshop/internal/order/ の中からしか見えません。

パッケージの命名

パッケージ名はすべての呼び出し箇所の一部になるので、ディレクトリのツリーより重要です。

  • 短く、小文字で、1単語: orderstorehttpapi。アンダースコアもmixedCapsも使いません。
  • 中身ではなく、提供するものの名前を付ける。 utilcommonhelpersmiscmodels は何も語らず、何でも放り込む場所になります。Goチームのスタイルガイダンスもこれらをはっきり勧めていません。ヘルパーはそれを使うパッケージに置くか、目的の名前を付けたパッケージ(slugretry)に置きます。
  • 名前の重複を避ける。 呼び出し側は order.Orderorder.New と書くので、order.OrderServiceorder.NewOrder のような名前にはしません。標準ライブラリはこれを一貫して守っており、http.HTTPServer ではなく http.Server です。
  • ディレクトリ名とパッケージ名を一致させる。 コマンドのディレクトリの package main は例外です。違っていると、インポートがどの識別子を導入するのかを知るために、読む人がファイルを開かなければなりません。

パッケージは種類ではなく責任で分けます。他のエコシステムでよく見る models/controllers/services/ という分け方をすると、どの機能も3つのパッケージに触れることになり、Goが禁止しているインポートの循環を生みがちです。ドメインの概念(orderpaymentuser)を中心にまとめたパッケージなら、それぞれが自分の型とロジックを一緒に持てます。

テストの置き場所

テストはコードの隣、同じディレクトリに *_test.go という名前のファイルで置きます。tests/ のツリーはありません。

  • order_test.go の中の package order:内部テストで、非公開の識別子を使えます。
  • 同じディレクトリの package order_test:外部テストで、呼び出し側と同じく公開されたAPIしか見えません。Exampleや、テストでのインポートの循環を避けるのに便利です。
  • フィクスチャのファイルはテストの隣の testdata/ ディレクトリに置きます。goツールはそれを無視し、テストはパッケージのディレクトリを作業ディレクトリとして実行されるので、testdata/big.json のような相対パスが動きます。

ルートにあるその他のファイル

ファイルやディレクトリ目的
go.modgo.sumモジュールの定義と依存関係のチェックサム。常にルートに置く
README.mdLICENSEどのプロジェクトとも同じ
MakefileTaskfile.yml省略可能なビルドの近道
Dockerfileサービスではたいていルートに置く
migrations/web/docs/Go以外の資産。中身に合わせて名前を付ける
tools.goツールの依存関係を固定する古い方法。Go 1.24では go.modtool 行(go get -tool)に置き換わった
go.work複数のモジュールを手元で一緒に開発するためのワークスペース。単一モジュールのプロジェクトでは普通コミットしない

よくある間違い

  • 小さなプロジェクトに大きなテンプレートをコピーする。 フラットに始め、2つ目のバイナリや本当の境界が現れたらディレクトリを加えます。
  • utilscommon という名前のパッケージ。 パッケージにはそれがすることの名前を付けます。
  • ファイルごと、型ごとに1つのパッケージ。 Goのパッケージは、Javaのクラスより大きな単位として意図されています。10個のファイルを持つパッケージは普通です。
  • インポートの循環。 2つのパッケージが互いをインポートすることはできません。たいていは、それらがひとまとまりであるべきか、共有の型をより低レベルのパッケージに移すべきか、一方がもう一方のパッケージではなく小さなインターフェースに依存するべきだということを意味します。
  • 反射的に pkg/ を使う。 意味を加えずに、すべてのインポートパスに要素を1つ増やします。
  • テストを別のディレクトリに置く。 そこからは非公開のコードが見えず、ツールもそれを想定していません。

よくある質問

Goに公式のプロジェクト構成はありますか?

必須のものはありません。Goチームはgo.dev/doc/modules/layoutでガイダンスを公開しており、いくつかのよくある形(単一のパッケージ、コマンド、internal/ を使う複数のコマンド)を説明しています。有名な golang-standards/project-layout リポジトリはコミュニティのプロジェクトであってGoの標準ではなく、Goチームも公にそう述べています。

Goのinternalディレクトリとは何ですか?

インポートパスに internal という要素を含むパッケージは、その internal ディレクトリの親をルートとするコードからしかインポートできません。example.com/app/internal/storeexample.com/app/ の下のどこからでもインポートできますが、他のモジュールからのインポートはgoコマンドが use of internal package ... not allowed で拒否します。

Goでpkgディレクトリは使うべきですか?

使わなくても構わず、意味を加えずにパスの要素を1つ増やすだけです。大きなプロジェクトの中には、公開ライブラリのコードを他と分けるために pkg/ を使うものもありますが、Goの標準ライブラリや最近のほとんどのプロジェクトは使っていません。インポートされるパッケージはモジュールのルートか名前付きのディレクトリに置き、非公開のものは internal/ に置きます。

Goのプロジェクトでテストファイルはどこに置きますか?

テストするコードと同じディレクトリに、*_test.go という名前で置きます。Goに別のテスト用のツリーはありません。フィクスチャのファイルはテストの隣の testdata ディレクトリに置き、goツールはパッケージを探すときにそれを無視します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める