go-project-layout について
go-project-layout は、Go 開発者やコーディングエージェント向けのプロジェクト構成スキルです。規模や種類に応じたレイアウトを提案し、パッケージ境界、依存関係、設定、検証を扱います。
主な機能
- 01規模に応じた構成ガイド
- 02モジュールとパッケージの命名規則
- 03サービスとライブラリの構成
- 04依存方向の原則
- 05設定と検証のチェックリスト
活用シーン
- 01小規模な Go ツールの開始
- 02Go サービスの構成設計
- 03インポート可能な Go ライブラリの整理
- 04モジュール境界の選定
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-project-layoutインストール後、Claude Code で/go-project-layoutを入力して読み込まれたか確認します。
詳細
Go プロジェクトのレイアウト
構成は規模に合わせます。Go でよくあるレイアウトの誤りは、500 行のツールにマイクロサービスのひな形をそのまま使うこと、あるいは 50 パッケージのサービスをフラットなディレクトリで拡張していくことです。プロジェクトに合ったレイアウトを選びましょう。
コミュニティのデフォルトです。
go-project-layoutを明示的に置き換えるプロジェクトスキルまたはエージェント設定ルールがある場合は、そちらが優先されます。
1. プロジェクトの規模に合わせてレイアウトを選ぶ
| プロジェクト | レイアウト |
|---|---|
| 小規模なツール、単一バイナリ、ファイル数 5 未満 | フラット:すべてルートの main パッケージに置く |
| 他のプロジェクトがインポートするライブラリ | モジュール名を付けたルートパッケージ、ヘルパーには internal/ |
| バイナリが 1 つのサービス | cmd/<name>/main.go + internal/ パッケージ |
| コードを共有する複数のバイナリ | cmd/<name1>/、cmd/<name2>/ + internal/ |
「後で使うから」と、空の pkg/、api/、docs/、build/ ディレクトリを作らないでください。
コードが必要とする段階で構成を追加し、先回りして作るのは避けましょう。
2. モジュール名
# ✅ Good — repository path, lowercase
go mod init github.com/acme/payment-service
# ❌ Bad — not fetchable, uppercase, or vanity without DNS
go mod init PaymentService
go mod init payment_service
最後のパス要素は、ユーザーに表示される名前と一致させます。ライブラリの場合、これがデフォルトのインポート名になります。
3. サービスのレイアウト(API とワーカーのデフォルト)
payment-service/
├── cmd/
│ └── payment-api/
│ └── main.go # flag/env parsing, wiring, Run() — nothing else
├── internal/
│ ├── domain/ # core types, business rules; zero external deps
│ ├── service/ # use cases orchestrating domain + stores
│ ├── store/ # data access implementations (postgres/, redis/)
│ ├── handler/ # HTTP/gRPC adapters
│ └── config/ # config loading and validation
├── migrations/ # if the service owns a database
├── go.mod
├── Makefile
└── README.md
ルール:
- デフォルトでは
internal/を使います。コンパイラーによって、モジュール外からのインポートが禁止されます。必要になった場合に限り、公開パッケージに昇格させます。 pkg/を使うのは、外部の利用者がいて、かつモジュールにプライベートコードもある場合に限ります。迷ったら作らないでください。- 依存関係は内側に向かわせます:
handler → service → domain ← store。domainはstoreもhandlerもインポートしません。
4. 薄い main と実行可能な Run
アプリをテスト可能にするため、main.go は配線と委譲呼び出しだけにします。
func main() {
if err := run(context.Background(), os.Args[1:], os.Getenv); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run(ctx context.Context, args []string, getenv func(string) string) error {
cfg, err := config.Load(getenv)
if err != nil {
return fmt.Errorf("load config: %w", err)
}
db, err := store.Open(ctx, cfg.DatabaseURL)
if err != nil {
return fmt.Errorf("open db: %w", err)
}
defer db.Close()
svc := service.New(store.NewUserRepo(db))
srv := handler.NewServer(cfg.Addr, svc)
return srv.ListenAndServe(ctx)
}
os.Exitが登場するのはmainの 1 か所だけです。runは依存関係(args、getenv)を受け取るため、テストから呼び出せます。- 配線に
init()関数を使わず、明示的な構築順序だけを使います。
5. ライブラリのレイアウト
retry/
├── retry.go # package retry — the API, in the root
├── retry_test.go
├── backoff.go # same package, split by topic
├── internal/
│ └── clock/ # implementation details users must not import
├── examples_test.go # Example* functions shown in godoc
└── go.mod
- ルートディレクトリ自体がパッケージです。
src/もlib/も使いません。 - 概念ごとに 1 つのパッケージにします。
util、common、helpersは避け、提供する機能にちなんだパッケージ名を付けます(retry、clock、httpsign)。
6. ディレクトリとパッケージの命名ルール
- パッケージ名はディレクトリ名と同じにし、短く、小文字で、アンダースコアを使いません:
store/postgresとし、store/postgres_implにはしません。 - 名前の重複を避けます:
payment.PaymentServiceではなくpayment.Serviceとします。 cmd/内のバイナリ名はユーザー向けです:cmd/payment-api。 ハイフンを使っても構いません(ディレクトリにはmainパッケージだけを置きます)。
7. ルートに置くファイル
go.mod、go.sum、README.md、LICENSE、Makefile、.golangci.yml、Dockerfile(単一バイナリのプロジェクト)。- 作成しないもの:
src/(慣用的ではありません)、vendor/(チームで明示的に依存関係をベンダー管理する場合を除く)、やがて何でも入れる場所になるtypes/やmodels/のような 1 ファイルだけのパッケージ。
8. 設定
- 設定用の構造体を 1 つ用意し、1 か所(
internal/config)で環境から読み込み、起動時に検証します。最初の 1 件だけでなく、欠落または無効な値をすべて列挙して、すぐに失敗させます。 - CLI にはフラグ、サービスには環境変数(12 ファクター)を使い、デフォルト値はコードに置きます。イメージに設定ファイルを含めたり、ビジネスコードに環境固有の分岐(
if env == "prod")を入れたりしないでください。 - シークレットは環境変数またはシークレットマネージャーから取得し、フラグ(
psで argv が表示されます)やリポジトリには置きません。
9. モジュールを 1 つにするか複数にするか
リポジトリごとにモジュールを 1 つにするのがデフォルトです。各部分を個別にバージョン管理し、リリースする場合(サービスと並ぶ公開クライアントライブラリなど)に限り、モジュールを分割します。go.work を使ってローカルで複数モジュールをまたいで開発し、replace => ../path ディレクティブはコミットしないでください。
スキャフォールディングの手順
- 尋ねる/決める:ツール、ライブラリ、サービスのどれですか?バイナリはいくつですか?
go mod init <repo-path>を実行し、goディレクティブを意図的に設定します — Go 1.26 以降では、go mod initはツールチェーンのバージョンではなく、 1 つ前のリリース(go 1.(N-1).0)を書き込みます。- 最初の機能に必要なディレクトリだけを作成します。
- 上記の薄い main パターンに従って
main.goを作成し、設定用の構造体を用意します。 - 開発ツールをモジュールツールとして固定し(Go 1.24 以降では
go get -tool ...)、Makefileにbuild、test、lintのターゲットを追加します。 - 確認します:スケルトンで
go build ./...とgo vet ./...が成功すること。
確認チェックリスト
- レイアウトがプロジェクトの規模に合っている — 空のスキャフォールディング用ディレクトリがない
- モジュールパスが取得可能なリポジトリパスである
- 公開されていないパッケージがすべて
internal/配下にある main.goが薄い:解析、配線、runの呼び出し、終了を行うos.Exitはmain内でのみ使用し、init()内で配線しない- 依存関係が内側に向かっている;
domainにインフラストラクチャのインポートがない - 雑多なパッケージとして
util/common/helpers/modelsを作成しない - パッケージ名がディレクトリ名と一致し、小文字で、名前が重複していない
- 新規スケルトンで
go build ./...が成功する - 起動時に1か所で設定を読み込み、検証する;フラグにシークレットを含めない
- 各パーツを個別にリリースする場合を除き、モジュールは1つにする;ローカルの
replaceをコミットしない
コメント
Markdown 対応