Go

Goのディレクトリ構成ベストプラクティス|project-layoutは公式標準ではない理由と最小構成

Goのディレクトリ構成を調べると、必ず golang-standards/project-layout というGitHubリポジトリに行き当たります。star数は56,443(2026年8月12日時点)。ところが、このリポジトリのREADMEは冒頭で自らを「Goコアチームが定めた公式標準ではない」と否定しています。この記事では、公式ドキュメントが実際に推奨している構成、主要Goリポジトリ15本を実測した採用実態、そして internalvendor がGoツールチェーンでどう扱われるかを、Go 1.26.5の実行結果とともに整理します。

まとめ

Goの公式ドキュメントに「標準ディレクトリ構成」は存在しません。公式が示しているのは Organizing a Go module というページで、そこで意味を持つディレクトリ名は internalcmd の2つだけです。pkgconfigsdeployments といった名前にツールチェーン上の意味はありません。

実務での結論を先に書きます。新規プロジェクトは go.modmain.go の2ファイルから始めてください。非公開にしたいコードが出てきた時点で internal/ を追加し、バイナリが2本以上になった時点で cmd/ を追加します。golang-standards/project-layout のroot直下19ディレクトリを最初から丸ごとコピーする運用は、README自身が「overkill(過剰)」と書いているとおり避けるべきです。実際、主要Goリポジトリ15本を調べたところ configsdeployments は1本も採用していませんでした。以下、この判断の根拠を順に示します。

golang-standards/project-layoutが公式標準ではない3つの根拠

Goのディレクトリ構成を調べる人の多くは、リポジトリ名そのものを検索して「これは何者で、従うべきなのか」を確かめに来ます。答えははっきりしています。従うべき公式規約ではありません。

README本文が明記する「NOT an official standard」の一文

根拠はリポジトリ自身のREADMEにあります。Overview節の2段落目は、太字コード表記の次の一文で始まります。

This is **`NOT an official standard defined by the core Go dev team`**.
This is a set of common historical and emerging project layout patterns
in the Go ecosystem.

同じ段落の後半では、Goコアチームが優れた一般指針を公開しているとして公式ドキュメントの Organizing a Go module を名指しで案内しています。続く段落では、学習中・PoC・個人プロジェクトに対して「このレイアウトは overkill であり、main.go 1本と go.mod があれば十分」と明言し、「クローンして必要なものだけ残し、あとは全部消せ」と結んでいます。リポジトリ自身の主張と「標準だから従う」という読み方は、最初から食い違っています。

Russ Coxがissue #117で指摘した「pkgは多数派ではない」

2021年4月9日、このリポジトリにissue #117「this is not a standard Go project layout」が立ちました。起票者はGitHubアカウント rsc、当時Goプロジェクトのテックリードを務めていたRuss Cox氏です。同issueはコメント255件を集め、2023年8月19日にクローズされました。主張は4点です。

  • 公式でないことはREADMEも認めているが、「エコシステムに共通の歴史的・新興のレイアウトパターン集」という自己規定そのものが不正確である
  • Goエコシステムの圧倒的多数のパッケージは、importできるパッケージを pkg サブディレクトリに置いていない
  • ここで説明されている構成は複雑すぎ、実際のGoリポジトリはもっと単純である
  • 「golang-standards」を名乗って提示されていることが不幸であり、実際には標準ではない

1点目が最も重い指摘です。「公式ではない」はREADMEも書いていますが、rsc氏はその先を突いて「慣習の集大成でもない」と述べています。起票の動機として「あなたは標準のGoプロジェクトレイアウトを使っていない」と言ってこのリポジトリを貼る人を見かけるようになったから、と本文に書かれています。なお golang-standards というGitHub Organizationは2017年8月28日作成・公開リポジトリ2件の独立した組織で、Go本体を管理する golang Organizationとは無関係です。

レイアウトを正面から扱う唯一の公式ページ「Organizing a Go module」

公式ドキュメント側で構成に触れているのは go.dev/doc/modules/layout の1ページだけです。ここが示す基本形は、ディレクトリを一切作らないフラット構成です。

project-root-directory/
  go.mod
  modname.go
  modname_test.go

そこから規模に応じて増やす方向として、同ページが挙げるディレクトリは internalcmd の2つだけです。しかも cmd は「コマンドとimport可能なパッケージが混在するリポジトリで非常に有用」という条件付きの推奨で、コマンドしかないリポジトリでは必須ではないと明記されています。pkgconfigsdeployments はディレクトリ名としては1か所も登場しません。

主要Goリポジトリ15本のトップ階層を実測した採用率

「実際のGoプロジェクトはもっと単純」という指摘が今も成り立つのかを、GitHub APIで各デフォルトブランチのルート直下ディレクトリを取得して確かめました。star数上位のGo製OSSから、CLI・サーバー・ライブラリ・インフラ基盤を混ぜて15本を選び、全リポジトリでルートに go.mod があることを確認しています。対象は kubernetes/kubernetes、prometheus/prometheus、grafana/grafana、gohugoio/hugo、moby/moby、etcd-io/etcd、hashicorp/terraform、gin-gonic/gin、labstack/echo、caddyserver/caddy、golang/go、docker/cli、containerd/containerd、minio/minio、traefik/traefik、取得日は2026年8月12日です。

internal 10本・cmd 8本・pkg 7本という採用実態

ディレクトリ名 15本中の採用数 公式Goドキュメントでの位置づけ
docs 11 言及なし
internal 10 公式で推奨・コンパイラが強制
cmd 8 公式で条件付き推奨
pkg 7 言及なし
scripts 6 言及なし
api 5 言及なし
vendor 4 go mod vendorの出力先
test 3 言及なし
tools 3 言及なし
build 1 言及なし
examples 1 言及なし
third_party 1 言及なし
src 1 GOPATHモードの構造(該当はコンパイラのソースツリー)
configs 0 言及なし
deployments 0 言及なし

公式ドキュメントが名指しする internalcmd が上位に来て、コンパイラ上の意味を持たない pkg が7本にとどまる分布です。labstack/echo に至っては、ルート直下のディレクトリが4つだけで cmdinternalpkg のいずれも持ちません。src を持つ唯一の1本は golang/go 本体で、これはGoコンパイラと標準ライブラリのソースツリーであってモジュール形式のアプリケーションではありません。

configs・deployments・third_partyが定着しない理由

採用ゼロだった configsdeployments、1本のみだった third_party には共通点があります。いずれもGoツールチェーンが何も解釈しないうえ、置かれる中身がGoコードですらありません。設定ファイルやKubernetesマニフェストの置き場所はGoのレイアウト問題ではなくリポジトリ運用の問題で、実際には deploy/manifests/charts/ など各プロジェクトに自然な名前が選ばれています。project-layout のroot直下19ディレクトリを先に作ると、新しく参加した開発者が「examples には何を置く決まりなのか」を毎回確認する手間だけが残ります。使わないディレクトリは作らない。実測から導ける最も実務的な結論です。

go.modとmain.goから始める最小構成と拡張の分岐点

ここからは実際に手を動かす順序です。Goプロジェクトの出発点は、ディレクトリを1つも作らない状態です。

$ mkdir myapp && cd myapp
$ go mod init example.com/myapp
$ cat > main.go
package main

import "fmt"

func main() { fmt.Println("hello") }

この2ファイルで go buildgo test も通ります。go mod init が生成する go.mod に書かれる go 1.26.5 のようなバージョン行は、単なる記録ではありません。この行がツールチェーンの選択やモジュールの挙動を左右します。バージョン行の意味と自動切り替えの仕組みは GOTOOLCHAINとは|Go Toolchainでバージョンを自動切り替えする仕組みと設定方法で扱っています。

単一パッケージのまま進められる範囲と分割の合図

ファイル数が10本を超えても、それだけでは分割の理由になりません。Goは同一ディレクトリの複数ファイルを1つのパッケージとして扱うため、auth.goclient.gomain.go をルートに並べる構成は公式ドキュメントが「basic command」として提示している形そのものです。

分割すべき合図は、ファイル数ではなく公開範囲です。外部に見せたくない実装が現れたとき、あるいは複数のコマンドから共有したいロジックが現れたときが境界になります。パッケージ境界の設計では、実装を隠しつつ振る舞いだけを公開するインターフェースの使い方が効いてきます。Goの暗黙実装の仕組みは Goのインターフェースとは?暗黙実装とダックタイピング・型制約を実装例で解説にまとめています。

cmdディレクトリを導入する条件と生成されるバイナリ名

バイナリが1本しかないうちは cmd/ は不要です。公式ドキュメントも「コマンドのみで構成されるリポジトリでは厳密には必要ない」と書いています。導入する条件は2つに絞れます。バイナリが2本以上あるか、import可能なパッケージとコマンドが同居しているか。どちらかに当てはまったときだけ作ってください。

導入すると、バイナリ名は cmd 直下のディレクトリ名がそのまま使われます。Go 1.26.5で cmd/toolx/main.go を用意して go install ./cmd/... を実行すると、GOBINtoolx という実行ファイルが生成されます。ディレクトリ名がユーザーから見えるコマンド名になるため、cmd/app1 のような仮の名前を残したままにしないでください。ウェブフレームワークを使う場合も同じで、ルーター初期化を cmd/server/main.go に置き、ハンドラの実体は別パッケージに逃がす形が扱いやすくなります。Ginでの最小構成は Gin(Go)とは?v1.12.0の最小構成・本番設定と標準net/httpとの使い分けで解説しています。

internalディレクトリの強制範囲|Go 1.26.5の実行結果

internal は、Goのディレクトリ名の中で唯一コンパイラが挙動を変える名前です。慣習ではなく規則である点が他のすべてのディレクトリと異なります。

外部モジュールからのimportが拒否される実際のエラー

規則は Go 1.4のリリースノートで導入され、次のように定義されています。「.../a/b/c/internal/d/e/f というパッケージは、.../a/b/c をルートとするディレクトリツリー内のコードからのみimportできる」。同ページには「Go 1.4ではGo本体のリポジトリにのみ強制され、1.5以降はすべてのリポジトリで強制される」という但し書きも付いています。自分のプロジェクトに効くようになったのはGo 1.5からです。実際にGo 1.26.5でモジュールをまたいでimportすると、ビルドは次のエラーで止まります。

$ go build ./...
package example.com/outside
	main.go:6:2: use of internal package example.com/demo/internal/auth not allowed

同じモジュール内の cmd/myapp/main.go からは問題なくimportでき、外部モジュールからだけ弾かれます。公開範囲の制御をレビューやドキュメントに頼らずに済むということです。公開APIとして維持する意思がないパッケージは、まず internal/ に置いてください。後から公開範囲を広げるのは容易ですが、一度公開したパッケージを非公開に戻すのは利用者への破壊的変更になります。

入れ子internalで公開範囲を階層ごとに絞る設計

internal はトップレベルに1つだけという制約もありません。任意の階層に置けて、そのつど親ディレクトリを基準に範囲が決まります。pkg/util/internal/secret を用意した状態をGo 1.26.5で確認すると、同じモジュール内であっても sub/consumer からのimportは use of internal package ... not allowed で拒否され、pkg/util からのimportだけが成功します。

モジュール全体には見せたくないが特定サブツリーでは共有したい、という中間的な公開範囲をディレクトリ配置だけで表現できます。マイクロサービスを1リポジトリに同居させる構成では、サービスごとに services/billing/internal/ を切ることで、隣のサービスから内部実装を直接呼ばれる事故をコンパイル時に止められます。

pkgディレクトリを作るかどうかの判断基準

pkg を作るべきかという問いは、Goのレイアウト論争で最も意見が割れる部分です。判断材料をはっきりさせておきます。

pkgにコンパイラ上の意味はないという事実

まず前提として、pkg という名前にツールチェーン上の効果は一切ありません。Go 1.26.5で pkg/util に置いたパッケージを別モジュールからimportすると、何の制約もなくビルドが成功します。internal の拒否エラーと対照的に、コンパイラは pkg を通常のディレクトリとしてしか見ていません。project-layout のREADME自体も pkg 節で「非公開パッケージを保証する手段としては internal のほうが優れている」「普遍的に受け入れられているわけではなく、Goコミュニティには推奨しない人もいる」と書いています。ただし同じ節は「そのディレクトリのコードが外部から使って安全だと明示的に伝える手段としては依然として有効」とも述べており、全否定はしていません。

この名前が広まった経緯も設計上の必然ではありません。READMEはBrad Fitzpatrick氏のツイートを引いて、初期のGoソースコードがパッケージ置き場に pkg を使っていたのをコミュニティが真似した結果だと説明しています。go help gopath の出力を見ると、GOPATHモードの pkg はコンパイル済みオブジェクト(pkg/GOOS_GOARCH/foo/bar.a)の置き場でした。ソースを入れる現在の用法は、元の意味とは別物です。

pkgが機能する条件とinternalを選ぶべき場面

採用してよい条件は1つだけです。リポジトリのルート直下にGo以外のコンポーネントが大量にあり、Goコードを1か所にまとめないと go コマンドの対象範囲が読みにくい場合です。フロントエンドのソース、Terraform定義、Helmチャートが同居するモノレポがこれに当たります。

逆に採用すべきでない場面も明確です。単一バイナリのサーバーアプリケーションで pkg/ を作るのは過剰です。公式ドキュメントがサーバー系プロジェクトについて「エクスポート用のパッケージを持つことは通常ない」「サーバーのロジックは internal に置くことを推奨する」と書いているとおり、外部から使われる予定のないコードを pkg に置くと、実装の詳細をどこまで安定させるべきかが曖昧になります。迷ったら internal を選んでください。制約が強い側から始めるほうが、後の設計変更が安全になります。

srcディレクトリを作らない理由とGOPATH時代との違い

「Goでは src を作らない」という話は結論としては正しいものの、理由が誤って伝わっていることがあります。ネストが深くなるからではありません。

go help gopathが示すsrcの本来の役割

src はGOPATHモードの構造上の要件でした。go help gopath の出力に「src ディレクトリはソースコードを保持する。src 以下のパスがimportパスまたは実行ファイル名を決定する」とあるとおり、GOPATHに指定したディレクトリは srcpkgbin という決まった構造を持つ必要があり、コードは $GOPATH/src/github.com/user/repo に置く以外の選択肢がありませんでした。importパスはこの物理配置から逆算されていました。

Go Modulesではこの対応関係が go.modmodule 行に移りました。module example.com/myapp と宣言すれば、リポジトリをディスク上のどこに置いてもimportパスは変わりません。src はGOPATHという仕組みに紐づいた要件であって、Goの美学ではないわけです。今から src/ を作ると、GOPATHの構造でもモジュールの構造でもない中途半端な階層が1つ増えるだけになります。

golang/goリポジトリにsrcがある理由

実測15本のうち唯一 src を持つ golang/go は、矛盾ではなく例外です。標準ライブラリのソースが src/net/http/ に置かれ、この物理配置がそのまま net/http というimportパスになっています。アプリケーションモジュールの参考にする対象ではないと切り分けてください。

vendorディレクトリの自動適用条件と不整合時に出る失敗

vendor/ は依存パッケージのソースをリポジトリに同梱する仕組みです。project-layout のREADMEでさえ「vendor パターンですら普遍的ではない」と書いており、実測15本でも採用は kubernetes、moby、docker/cli、containerd の4本にとどまりました。

go mod vendorが生成するファイルとmodules.txtの役割

手作業でコピーするものではなく go mod vendor が生成します。出力は依存パッケージのソースツリーと、vendor/modules.txt という目録ファイルの2つです。Go 1.26.5で実行すると modules.txt にはモジュールパス・バージョン・replace の解決先・go ディレクティブのバージョン・vendorへ入れたパッケージ一覧が記録されます。この目録と go.mod が一致しているかどうかが、次に述べる判定に使われます。

go 1.14以降の自動-mod=vendorと不整合エラーの読み方

-mod=vendor フラグを明示する必要は、もうありません。go.modgo ディレクティブが1.14以上で、かつ vendor/modules.txt が存在すれば、go build は自動的にvendorディレクトリを参照します。Go 1.26.5で go list -f '{{.Dir}}' を叩くと、依存のパスがモジュールキャッシュではなく vendor/ 配下として返ってきます。

この自動化は、失敗したときのエラーが分かりにくいという副作用を持ちます。vendorの中身が go.mod とずれた状態でビルドすると、次のように「モジュールが見つからない」系のメッセージが出ます。

cmd/toolx/main.go:6:2: cannot find module providing package example.com/lib: import lookup disabled by -mod=vendor
	(Go version in go.mod is at least 1.14 and vendor directory exists.)

ネットワークやプロキシの問題に見えますが、原因はvendorの不整合です。指定した覚えのない -mod=vendor が効いていること自体がヒントになっています。依存を追加・更新したら go mod vendor を再実行してコミットする、これをCIで検証する。この2点を運用に入れておけば防げます。vendorを使わない場合は vendor/ をディレクトリごと削除してください。中途半端に残っているのが一番危険な状態です。

goツールが走査から除外する名前の規則

最後に、レイアウトを決めるうえで知っておくと事故が減る仕様を挙げます。go コマンドは特定の名前をパッケージ探索の対象から外します。

testdata・アンダースコア・ドットが除外される仕組み

go help packages の記述はこうです。「ドットまたはアンダースコアで始まるディレクトリ名およびファイル名は go ツールに無視される。testdata という名前のディレクトリも同様である」。ディレクトリだけでなくファイル名も対象である点に注意してください。_helper.go という名前を付けた時点で、そのファイルはビルドされません。

Go 1.26.5で testdata/_ignored/.hidden/ のそれぞれに構文エラーを含むGoファイルを置いても、go build ./... は正常終了します。go list ./... の出力にもこれらは現れません。テスト用のフィクスチャや、ビルド対象にしたくない実験コードの置き場として使える仕様です。vendor/ も別ルールで除外され、./..../vendor 配下のパッケージにマッチしません。

アンダースコア始まりで静かにスキップされる事故

裏を返せば、通常のパッケージをうっかり _internal/ のような名前に置くと、ビルドもテストも静かにスキップされます。エラーは出ません。「テストが通っている」のではなく「テストが実行されていない」状態になるため、パッケージのディレクトリ名・ファイル名の先頭にアンダースコアやドットを使わないでください。

よくある質問

golang-standards/project-layoutは使ってはいけないのですか?

禁止すべきものではなく、「公式標準として無条件に従うもの」ではないというだけです。README自身がクローンして必要なものだけ残し、残りは削除するよう案内しています。大規模なサーバーアプリケーションで cmdinternalapi だけを採り入れるといった使い方は妥当です。避けるべきなのは、root直下19個のディレクトリを空のまま作って「標準に準拠した」と考える運用です。

Goのプロジェクトでcmdディレクトリは必須ですか?

必須ではありません。公式ドキュメントは、コマンドだけで構成されるリポジトリでは cmd は厳密には不要と書いています。導入の目安はバイナリが2本以上になったとき、またはimport可能なパッケージとコマンドが同じリポジトリに同居しているときです。バイナリ1本のツールなら、ルートに main.go を置く構成で問題ありません。

internalとpkgはどちらを使うべきですか?

迷ったら internal です。internal は外部モジュールからのimportをコンパイラが拒否しますが、pkg という名前にツールチェーン上の効果はありません。公式ドキュメントもサーバー系プロジェクトのロジックは internal に置くよう推奨しています。pkg が有効なのは、Go以外のコンポーネントが多いリポジトリでGoコードを1か所にまとめたい場合に限られます。

Goでsrcディレクトリを作らないのはなぜですか?

src はGOPATHモードでimportパスを決めるための構造上の要件で、Go Modulesではその役割が go.modmodule 行に移ったためです。go help gopath には今も「src 以下のパスがimportパスまたは実行ファイル名を決定する」と記載されています。モジュール環境で src/ を作っても、importパスには影響せず階層が1つ深くなるだけです。

vendorディレクトリはコミットすべきですか?

アプリケーションでは、ビルドの再現性やネットワーク制限のある環境が要件なら有効です。ライブラリではコミットすべきではありません。採用する場合は go mod vendor の実行結果をそのままコミットし、依存を更新するたびに再生成してください。go.modgo ディレクティブが1.14以上かつ vendor/modules.txt があれば、ビルドは自動的にvendorを参照します。

関連記事

資料請求

RELATED POSTS 関連記事