Goプロジェクトの構造:実践とパターン

スケーラビリティと明確さを確保するためのGoプロジェクトの構造設計

目次

Go プロジェクトの構造を適切に整えることは、長期的な保守性、チームの協業、そしてスケーラビリティにとって不可欠です。厳格なディレクトリ構成を強制するフレームワークとは異なり、Go は柔軟性を重視しています。しかし、その自由度には、プロジェクトの具体的なニーズに合ったパターンを選択する責任が伴います。

project tree

Go のプロジェクト構造に関する哲学を理解する

Go のミニマリストな設計哲学は、プロジェクトの組織化にも及んでいます。この言語は特定の構造を強制するのではなく、開発者が情報に基づいた判断を下すことを信頼しています。このアプローチにより、コミュニティは小規模なプロジェクト向けのシンプルなフラットなレイアウトから、エンタープライズシステム向けの高度なアーキテクチャに至るまで、いくつかの実証済みのパターンを生み出してきました。

最も重要な原則は 「シンプルを第一に、必要に応じて複雑さを取り入れる」 というものです。多くの開発者は、初期の構造を過剰に設計し、深くネストしたディレクトリや早期の抽象化を作成するという罠に落ちがちです。今日必要なものから始め、プロジェクトが成長するにつれてリファクタリングを行ってください。

internal/ ディレクトリと pkg/ の使い分け

internal/ ディレクトリは、Go の設計において特定の役割を果たします。このディレクトリには、外部プロジェクトからインポートできないパッケージが含まれます。Go のコンパイラはこの制限を強制するため、internal/ は再利用を目的としないプライベートなアプリケーションロジック、ビジネスルール、ユーティリティに最適です。

一方、pkg/ ディレクトリは、コードが外部での使用を意図していることを示します。ライブラリや、他者がインポートすることを望む再利用可能なコンポーネントを構築している場合にのみ、ここにコードを配置してください。多くのプロジェクトでは pkg/ をまったく必要としません。コードが外部で消費されない場合、ルートまたは internal/ に保持しておく方がクリーンです。再利用可能なライブラリを構築する際には、型安全性が高く再利用可能なコンポーネントを作成するために、Go ジェネリクス を活用することを検討してください。

標準的な Go プロジェクトのレイアウト

最も広く認識されているパターンは golang-standards/project-layout ですが、これは公式の標準ではない点に注意が必要です。一般的な構造は以下のようになります。

myproject/
├── cmd/
│   ├── api/
│   │   └── main.go
│   └── worker/
│       └── main.go
├── internal/
│   ├── auth/
│   ├── storage/
│   └── transport/
├── pkg/
│   ├── logger/
│   └── crypto/
├── api/
│   └── openapi.yaml
├── config/
│   └── config.yaml
├── scripts/
│   └── deploy.sh
├── go.mod
├── go.sum
└── README.md

多くのチームは、cmd/ 内のバイナリが起動時に1つのロガーを接続できるように、小さな共有ロギングパッケージを internal/ の下(例:internal/logx)に保持します。上記のスケッチにある pkg/logger/ は、そのコードが他のモジュールからインポートされることを意図している場合にのみ適切です。本番環境向けの log/slog セットアップ(JSON ライン形式、機密情報の削除、コンテキストとトレースフィールド、モニタリングスタックと連携するシグナルなど)については、Go における slog を使った構造化ログ:可観測性とアラートのため を参照してください。

cmd/ ディレクトリ

cmd/ ディレクトリには、アプリケーションのエントリポイントが含まれます。各サブディレクトリは、別の実行可能バイナリを表します。例えば、cmd/api/main.go は API サーバーをビルドし、cmd/worker/main.go はバックグラウンドジョブプロセッサをビルドします。

ベストプラクティス: main.go ファイルは最小限に保ってください。依存関係の接続、設定の読み込み、アプリケーションの開始に必要なものだけを記載します。すべての実質的なロジックは、main.go がインポートするパッケージに属します。

// cmd/api/main.go
package main

import (
    "log"
    "myproject/internal/server"
    "myproject/internal/config"
)

func main() {
    cfg, err := config.Load()
    if err != nil {
        log.Fatal(err)
    }
    
    srv := server.New(cfg)
    if err := srv.Start(); err != nil {
        log.Fatal(err)
    }
}

internal/ ディレクトリ

ここでは、プライベートなアプリケーションコードが配置されます。Go コンパイラは internal/ 内のパッケージからのインポートを外部プロジェクトで防止するため、以下に最適です。

  • ビジネスロジックとドメインモデル
  • アプリケーションサービス
  • 内部 API とインターフェース
  • データベースリポジトリ(適切な ORM の選択については、PostgreSQL 用 Go ORM の比較 を参照)
  • 認証と認可ロジック

internal/ は、技術的なレイヤー(internal/handlers/internal/services/internal/repositories/ など)ではなく、機能やドメインごとに整理してください。代わりに internal/user/internal/order/internal/payment/ のようにし、各パッケージにハンドラ、サービス、リポジトリを含めます。

複雑なディレクトリ構造から始めるべきか?

絶対にそうすべきではありません。小規模なツールやプロトタイプを構築している場合は、以下から始めます。

myproject/
├── main.go
├── go.mod
└── go.sum

プロジェクトが成長し、論理的なグループ化が明確になった時点で、ディレクトリを導入します。データベースロジックが本格的になった時点で db/ パッケージを追加したり、HTTP ハンドラが増えた時点で api/ パッケージを追加したりします。事前に構造化を強要するのではなく、自然に構造が形成されるようにします。

フラット構造 vs ネスト構造:バランスを見つける

Go プロジェクトの構造における最も一般的なミスの1つは、過度なネストです。Go は浅い階層を好みます。通常は1または2レベルまでです。深いネストは認知負荷を増加させ、インポートを煩雑にします。

最も一般的なミスは何か?

ディレクトリの過度なネスト: internal/services/user/handlers/http/v1/ のような構造を避けてください。これにより、不要なナビゲーションの複雑さが生じます。代わりに internal/user/handler.go を使用します。

一般的なパッケージ名: utilshelperscommonbase といった名前はコードスメルです。それらは特定の機能を示唆せず、関連のないコードの捨て場所となりがちです。記述的な名前を使用してください。validatorauthstoragecache などです。

循環依存: パッケージ A がパッケージ B をインポートし、B が A をインポートする場合、循環依存が生じます。これは Go におけるコンパイルエラーです。これは通常、懸念事項の分離が不十分であることを示しています。インターフェースを導入するか、共有タイプを別パッケージに抽出してください。

懸念事項の混同: HTTP ハンドラは HTTP に関する懸念事項に集中させ、データベースリポジトリはデータアクセスに集中させ、ビジネスロジックはサービスパッケージに配置します。ハンドラにビジネスルールを配置すると、テストが困難になり、ドメインロジックが HTTP に結合してしまいます。

ドメイン駆動設計とヘキサゴナルアーキテクチャ

大規模なアプリケーション、特にマイクロサービスの場合、ドメイン駆動設計(DDD)とヘキサゴナルアーキテクチャは、懸念事項の明確な分離を提供します。

ドメイン駆動設計に従ってマイクロサービスをどのように構成するか?

ヘキサゴナルアーキテクチャは、依存関係が内側に向かう同心円状のレイヤーでコードを整理します。

internal/
├── domain/
│   └── user/
│       ├── entity.go        # ドメインモデルと値オブジェクト
│       ├── repository.go    # リポジトリインターフェース(ポート)
│       └── service.go       # ドメインサービス
├── application/
│   └── user/
│       ├── create_user.go   # ユースケース: ユーザー作成
│       ├── get_user.go      # ユースケース: ユーザー取得
│       └── service.go       # アプリケーションサービスのオーケストレーション
├── adapter/
│   ├── http/
│   │   └── user_handler.go  # HTTP アダプター(REST エンドポイント)
│   ├── postgres/
│   │   └── user_repo.go     # データベースアダプター(リポジトリポートを実装)
│   └── redis/
│       └── cache.go         # キャッシュアダプター
└── api/
    └── http/
        └── router.go        # ルート設定とミドルウェア

ドメインレイヤー (domain/): コアビジネスロジック、エンティティ、値オブジェクト、ドメインサービスインターフェース。このレイヤーは外部システムへの依存関係を持ちません。HTTP やデータベースのインポートはありません。アダプターが実装するリポジトリインターフェース(ポート)を定義します。

アプリケーションレイヤー (application/): ドメインオブジェクトをオーケストレートするユースケース。各ユースケース(例:「ユーザー作成」、「支払い処理」)は個別のファイルまたはパッケージです。このレイヤーはドメインオブジェクトを調整しますが、それ自体にはビジネスルールは含みません。

アダプターレイヤー (adapter/): 内側のレイヤーで定義されたインターフェースを実装します。HTTP ハンドラはリクエストをドメインオブジェクトに変換し、データベースリポジトリは永続化を実装し、メッセージキューは非同期通信を処理します。このレイヤーには、すべてのフレームワーク固有およびインフラストラクチャコードが含まれます。

API レイヤー (api/): ルート、ミドルウェア、DTO(データ転送オブジェクト)、API バージョニング、OpenAPI 仕様。

この構造により、コアビジネスロジックがテスト可能であり、フレームワーク、データベース、または外部サービスから独立したまま保たれます。PostgreSQL を MongoDB に、REST を gRPC に交換しても、ドメインコードをいじる必要はありません。このレイアウト内で CQRS を採用する場合、Go における CQRS の実装 では、application/ レイヤーがコマンドとクエリのハンドラパッケージに自然にマッピングされる方法、コマンド側を厳密に保ちつつクエリ側を DTO 指向に保つ方法が示されています。

異なるプロジェクトタイプ向けの実用的なパターン

小規模な CLI ツール

コマンドラインアプリケーションの場合、複数のコマンドとサブコマンドをサポートする構造が望ましいでしょう。コマンド構造には Cobra、設定管理には Viper などのフレームワークの使用を検討してください。Cobra と Viper を使った Go での CLI アプリケーションの構築 に関するガイドでは、このパターンについて詳しく説明しています。

mytool/
├── main.go
├── command/
│   ├── root.go
│   └── version.go
├── go.mod
└── README.md

REST API サービス

Go で REST API を構築する場合、この構造は懸念事項を明確に分離します。ハンドラは HTTP に関する懸念事項を処理し、サービスはビジネスロジックを含み、リポジトリはデータアクセスを管理します。標準ライブラリのアプローチ、フレームワーク、認証、テストパターン、本番環境向けのベストプラクティスを網羅する包括的なガイドについては、Go での REST API 構築の完全ガイド を参照してください。構造化 JSON ロギング、リクエストとトレースの相関、アラートをサポートするログ形状については、Go における slog を使った構造化ログ:可観測性とアラートのため を参照してください。

myapi/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── config/
│   ├── middleware/
│   ├── user/
│   │   ├── handler.go
│   │   ├── service.go
│   │   └── repository.go
│   └── product/
│       ├── handler.go
│       ├── service.go
│       └── repository.go
├── pkg/
│   └── httputil/
├── go.mod
└── README.md

複数のサービスを含むモノレポ

myproject/
├── cmd/
│   ├── api/
│   ├── worker/
│   └── scheduler/
├── internal/
│   ├── shared/        # 共有内部パッケージ
│   ├── api/          # API 固有のコード
│   ├── worker/       # Worker 固有のコード
│   └── scheduler/    # Scheduler 固有のコード
├── pkg/              # 共有ライブラリ
├── go.work           # Go ワークスペースファイル
└── README.md

テストとドキュメント

テストファイルは、テスト対象のコードの横に _test.go サフィックスを使用して配置します。

internal/
└── user/
    ├── service.go
    ├── service_test.go
    ├── repository.go
    └── repository_test.go

この慣例により、テストは実装の近くに保持され、見つけやすく、保守しやすくなります。複数のパッケージにまたがる統合テストについては、プロジェクトのルートに別々の test/ ディレクトリを作成します。テーブル駆動テスト、モック、カバレッジ分析、ベストプラクティスを含む、効果的な単体テストの書き方に関する包括的なガイダンスについては、Go の単体テストの構造とベストプラクティス を参照してください。

ドキュメントは以下に配置します。

  • README.md: プロジェクトの概要、セットアップ手順、基本的な使用方法
  • docs/: 詳細なドキュメント、アーキテクチャ決定、API リファレンス
  • api/: OpenAPI/Swagger 仕様、protobuf 定義

REST API では、Swagger を使用して OpenAPI ドキュメントを生成し、提供することは、API の発見可能性と開発者体験にとって不可欠です。Go API に Swagger を追加する に関するガイドでは、人気のあるフレームワークとの統合とベストプラクティスをカバーしています。

Go Modules による依存関係の管理

すべての Go プロジェクトでは、依存関係の管理に Go Modules を使用する必要があります。Go コマンドとモジュール管理の包括的なリファレンスについては、Go チートシート をチェックしてください。以下で初期化します。

go mod init github.com/yourusername/myproject

これにより、go.mod(依存関係とバージョン)と go.sum(検証用チェックサム)が作成されます。再現可能なビルドのために、これらのファイルをバージョン管理に保持してください。

定期的に依存関係を更新します。

go get -u ./...          # すべての依存関係を更新
go mod tidy              # 未使用の依存関係を削除
go mod verify            # チェックサムを検証

重要なポイント

  1. シンプルに始め、自然に進化させる: 初期構造を過剰に設計しないでください。複雑さがそれを要求する際に、ディレクトリとパッケージを追加してください。

  2. フラットな階層を優先する: ネストは1または2レベルに制限してください。Go のフラットなパッケージ構造は読みやすさを向上させます。

  3. 記述的なパッケージ名を使用する: utils のような一般的な名前は避けてください。パッケージには、それが行うものに基づいた名前を付けます。authstoragevalidator などです。

  4. 懸念事項を明確に分離する: ハンドラは HTTP に、リポジトリはデータアクセスに、ビジネスロジックはサービスパッケージに集中させます。

  5. プライバシーのために internal/ を活用する: 外部からインポートされるべきではないコードに使用します。ほとんどのアプリケーションコードはここに属します。

  6. 必要に応じてアーキテクチャパターンを適用する: 複雑なシステムの場合、ヘキサゴナルアーキテクチャと DDD は明確な境界とテスト可能性を提供します。

  7. Go に任せる: 他の言語からパターンをインポートするのではなく、Go のイディオムに従ってください。Go には、シンプルさと組織化に関する独自の哲学があります。

関連リンク

その他の関連記事

購読する

システム、インフラ、AIエンジニアリングの新記事をお届けします。