Goプロジェクト構成: ベストプラクティスとパターン

スケーラビリティと明確性のためにGoプロジェクトを構成しましょう

目次

Goプロジェクトの構成は、長期的な保守性、チームの協力、スケーラビリティにとって基本的な要素です。フレームワークが厳格なディレクトリ構造を強制するのとは異なり、Goは柔軟性を重視していますが、その自由にはプロジェクトの特定のニーズに応じたパターンを選択する責任が伴います。

プロジェクトツリー

Goにおけるプロジェクト構成の哲学の理解

Goのミニマリストデザイン哲学はプロジェクトの構成にも適用されます。この言語は特定の構造を義務付けることはなく、開発者が情報をもとに判断するように信頼しています。このアプローチにより、コミュニティは小規模なプロジェクト向けの単純なフラット構造から、企業向けの高度なアーキテクチャまで、いくつかの確立されたパターンを開発してきました。

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

internal/ディレクトリとpkg/ディレクトリの使用タイミング

internal/ディレクトリはGoの設計において特定の目的を持っています。これは、外部プロジェクトからインポートされないパッケージを含みます。Goのコンパイラはこの制限を強制するため、internal/は外部に公開されたくないアプリケーション論理、ビジネスルール、ユーティリティに最適です。

一方、pkg/ディレクトリはコードが外部から利用されるものであることを示します。ライブラリや再利用可能なコンポーネントを作成し、他者がインポートしたい場合にのみここにコードを配置してください。多くのプロジェクトではpkg/は必要ありません。コードが外部から利用されない場合は、ルートまたはinternal/に保つのがより潔いです。再利用可能なライブラリを構築する際には、Go genericsを活用して、型安全な再利用可能なコンポーネントを作成してください。

標準的な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/ディレクトリ

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/パッケージを追加してください。構造は自然に現れるようにして、最初から強制的に構造を課すのを避けてください。

フラット構造とネスト構造:バランスの取れた方法

Goプロジェクト構造において最も一般的なミスの一つは、過剰なネストです。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に変更してもドメインコードを変更することなく行えます。

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

小規模なCLIツール

コマンドラインアプリケーションでは、複数のコマンドとサブコマンドをサポートする構造が必要です。Cobraを使用してコマンド構造を、Viperを使用して設定管理を行うことを検討してください。GoでCobraとViperを使用したCLIアプリケーションの構築ではこのパターンを詳細に説明しています。

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

REST APIサービス

GoでREST APIを構築する際には、ハンドラがHTTPの関心を、サービスがビジネス論理を、リポジトリがデータアクセスを担当するように明確に分離します。標準ライブラリのアプローチ、フレームワーク、認証、テストパターン、生産性に向けたベストプラクティスについて網羅的に説明するガイドについては、GoでREST APIを構築する完全ガイドを参照してください。

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/       # ワーカー固有のコード
│   └── 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モジュールを使用した依存関係管理

すべてのGoプロジェクトは依存関係管理にGoモジュールを使用する必要があります。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ハンドラはHTTPに集中し、リポジトリはデータアクセスに集中し、ビジネス論理はサービスパッケージに含めましょう。

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

  6. 必要に応じてアーキテクチャパターンを適用する: 複雑なシステムでは、六角形アーキテクチャとDDDは明確な境界とテスト性を提供します。

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

有用なリンク

他の関連記事