Goプロジェクトの意見のある構造
Emily Parker
Product Engineer · Leapcell

はじめに
活気のあるGoエコシステムにおいて、プロジェクト構造のトピックはしばしば活発な議論を巻き起こします。多くの新規参入者や経験豊富な開発者にとって、「標準Goプロジェクトレイアウト」リポジトリは事実上の参照点となっています。これは、大規模で多面的なGoプロジェクトの共通基盤を提供することを目的として、包括的なディレクトリ構造を規定しています。このイニシアチブは、ベストプラクティスを標準化しようと称賛に値しますが、実際には、ほとんどのWebアプリケーションにとっては過剰であったり、逆効果でさえある青写真を提供します。この記事では、この「標準」レイアウトを盲目的に採用することが、特にWebサービスを構築する際に、Goが提唱するシンプルさと機敏性にとってどのように有害であるかを掘り下げます。代わりに、Web開発の反復的な性質と典型的な規模によく適合する、より適切な代替アプローチを探ります。
Goプロジェクト構造の解読
さらに深く掘り下げる前に、議論するコアコンセプトについて共通の理解を確立しましょう。
「標準Goプロジェクトレイアウト」
この広く引用されているリポジトリは、非常に意見のある階層構造を提案しています。その主な目標は、プロジェクトを明確な責任に編成することです。
cmd/: プロジェクトのメインアプリケーションを含みます。cmd内の各ディレクトリは独立した実行ファイルです。pkg/: 他のプロジェクトで一般的に使用されることを意図したライブラリコード。internal/: 他のプロジェクトからインポートできないプライベートなアプリケーションおよびライブラリコード。api/: API定義(例: OpenAPI、Protobuf)。web/: Webアプリケーション固有のコンポーネント(例: 静的アセット、テンプレート)。configs/: 設定ファイルテンプレートまたはデフォルト設定。build/: パッケージングおよび継続的インテグレーション。deployments/: IaaS、PaaS、コンテナオーケストレーションのデプロイメント設定。scripts/: さまざまなビルド、インストール、分析、その他の管理タスクを実行するためのスクリプト。test/: 外部テストアプリケーションおよび追加のテストデータ。
このレイアウトの根拠は、特にモノレポや複数の独立した実行ファイルおよび共有内部ライブラリを持つプロジェクトで、プロジェクトの複雑さに合わせてスケーリングする、包括的なエンタープライズグレードの構造を提供することです。
Webアプリケーションに適さない理由
多くのWebアプリケーション、特にモダンなマイクロサービスやAPIファーストのパラダイムに従うものは、より集中しています。これらは通常、HTTPリクエストの処理、データベースとの対話、そしておそらくいくつかの外部サービスとの通信といった単一の主要な目的に役立ちます。「標準Goプロジェクトレイアウト」が負担となる具体的な理由を見てみましょう。
1. シンプルさのための過剰なアーキテクチャ
「少ないほど多い」と要約されるGoの哲学は、シンプルで明示的な設計を奨励します。複雑なディレクトリ構造は認知的オーバーヘッドを追加します。1つまたは2つのメイン実行ファイル(cmd/api、cmd/worker)を持つ典型的なWeb APIにとって、cmd、pkg、internalの分離は無理に感じられるかもしれません。
シンプルなWebサービスを考えてみましょう。
// main.go package main import ( "log" "net/http" ) func helloHandler(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, world!")) } func main() { http.HandleFunc("/", helloHandler) log.Fatal(http.ListenAndServe(":8080", nil)) }
これをcmd/api/main.goに配置し、pkgとinternalディレクトリが空であるか、ほとんど内容がない場合、実際のコードに対して不釣り合いに感じられます。pkgとinternalの値は、同じリポジトリ内の複数の独立したアプリケーションによって実際に共有される再利用可能なコンポーネントがある場合、または特定のパッケージの外部インポートを明示的に禁止したい場合に輝きます。単一のWebサービスでは、「internal」の区別は主に、独自のプロジェクト構造に対する間接的なレイヤーを追加します。
2. internal パッケージの議論
internalディレクトリは、「標準レイアウト」の核となる原則です。Goのコンパイラは、internalディレクトリ内のパッケージは、internalが存在するモジュール外のコードからインポートできないことを強制します。これはモジュール性を強制するための強力な機能です。しかし、自己完結型のWebサービスでは、アプリケーションロジックのほぼすべてがそのサービス内部のものです。すべてのドメインモデル、サービスハンドラ、リポジトリを個別のinternalサブディレクトリに配置することは、中小規模のプロジェクトでは明確なメリットなしにコードベースを断片化させることがよくあります。
一般的なWebアプリケーション構造で例示しましょう。
標準レイアウトアプローチ:
my-webapp/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── auth/
│ │ └── service.go
│ ├── handlers/
│ │ └── user.go
│ ├── models/
│ │ └── user.go
│ └── repository/
│ └── user.go
└── go.mod
ここで、cmd/api/main.goはinternal/auth、internal/handlersなどをインポートします。機能的には正しいですが、internalプレフィックスは、単一サービスプロジェクトでの外部インポートを防ぐための必要性よりも、組織的な慣習として機能します。
代替(フラット)アプローチ:
my-webapp/
├── main.go
├── auth/
│ └── service.go
├── handlers/
│ └── user.go
├── models/
│ └── user.go
├── repository/
│ └── user.go
└── go.mod
このよりフラットな構造では、モジュール(auth、handlers、``models、repository)はプロジェクトルートの直下にあります。これらは他のモジュール(ライブラリの場合)によって技術的にインポート可能ですが、スタンドアロンアプリケーションでは通常問題ありません。Goコミュニティでは、これを「フラット」または「パッケージごとの機能」レイアウトと呼ぶことがよくあります。これは、Goの強力なパッケージ境界を利用してインターフェースを定義し、実装の詳細を非表示にし、追加のinternal`レイヤーなしで行われます。
3. プロジェクト編成の時期尚早な最適化
最初から複雑なレイアウトを採用することは、時期尚早な最適化の一形態となり得ます。それはまだプロジェクトに存在しない問題を解決するかもしれません。Webアプリケーションが進化するにつれて、コードのリファクタリングと再編成は開発プロセスにおける自然な部分です。よりシンプルな構造から始めることで、有機的な成長と適応が可能になります。
たとえば、すべてのHTTPハンドラ用に最初はhandlersパッケージがある場合:
my-webapp/
├── main.go
├── handlers/
│ ├── user.go
│ └── product.go
└── go.mod
プロジェクトが成長し、user.goとproduct.goが大きくなりすぎると、機能ごとにグループ化することを決定するかもしれません。
my-webapp/
├── main.go
├── user/
│ ├── handler.go
│ └── service.go
├── product/
│ ├── handler.go
│ └── service.go
└── go.mod
新しいパッケージをpkg、internal、または新しいcmdエントリのいずれに配置するかについて常に議論する必要がない場合、この進化は管理が容易になります。
4. 「標準」は権威を意味する
「標準Goプロジェクトレイアウト」というタイトルの自体は、かなりの重みを持っています。新規参入者は、これをGoプロジェクトを構造化する唯一の正しい方法と見なす可能性があり、シンプルなWebサービスに不必要な複雑さをもたらします。Goチーム自体(言語の作成者)は、公式のプロジェクト構造を支持しておらず、個々のプロジェクトに委ねることを好みます。golang-standards組織はコミュニティ主導であり、公式のGoプロジェクトではありません。この区別は重要です。
Webアプリケーションのためのより実践的なアプローチ
多くの成功したGo Webアプリケーションは、よりシンプルで実用的なレイアウトを選択しています。一般的なパターンは次のとおりです。
-
フラット構造 / パッケージごとの機能: 機能またはドメインコンセプトの名前が付けられたトップレベルパッケージに、関連するコードをグループ化します。
my-webapp/ ├── main.go # エントリーポイント ├── config/ # アプリケーション設定 ├── server/ # HTTPサーバー設定、ミドルウェア ├── user/ # ユーザー関連ドメイン、サービス、ハンドラ、リポジトリ │ ├── handler.go │ ├── service.go │ └── repository.go ├── product/ # 製品関連ドメイン、サービス、ハンドラ、リポジトリ │ ├── handler.go │ └── service.go ├── common/ # ユーティリティ関数、共有インターフェース └── go.modこのアプローチは、機能に関連するすべてのコードをまとめて、検出可能性と結束性を向上させます。
main.goがすべてを配線します。 -
フラット構造内でのレイヤード/ヘキサゴナル(ポート&アダプター): より厳密に懸念事項を分離したい大規模なWebアプリケーションの場合、レイヤードアーキテクチャをフラット構造内で実装できます。
my-webapp/ ├── main.go ├── config/ ├── internal/ # アプリケーション固有のコアロジック(ドメイン、ユースケース、サービス) │ ├── domain/ │ │ ├── user.go │ │ └── product.go │ ├── service/ # ビジネスロジック、ドメインオブジェクトのオーケストレーション │ │ ├── user.go │ │ └── product.go │ └── ports/ # 外部依存関係(データベース、HTTP、メッセージキュー)のインターフェース │ ├── database.go │ └── http.go ├── adapters/ # ポートの実装(例: データベース用のGORM、HTTP用のGin/Echo) │ ├── http/ │ │ └── handler.go │ └── persistence/ │ └── postgres.go └── go.modここでは、
internalディレクトリは、外部依存関係を直接持ったり、公開されたりするべきではないコアアプリケーションロジックに使用されます。次に、adaptersがこのコアを外部世界に接続します。これは、internalのはるかに具体的で意味のある使用法です。
重要なのは、あなたのプロジェクトの現在の規模と複雑さに最も適した構造を選択し、それが自然に進化できるようにすることです。シンプルに始め、実際に遭遇している問題を解決する場合にのみ複雑さを導入してください。
結論
「標準Goプロジェクトレイアウト」は、複雑で複数の実行ファイルを持つプロジェクトやモノレポに適した、包括的ではあるが意見のある構造を提供しますが、典型的なWebアプリケーションには不必要な複雑さをもたらすことがよくあります。Goの強みは、そのシンプルさと明示的な設計にあります。プロジェクト構造はこれらの価値観を反映すべきです。ほとんどのWebサービスでは、よりフラットで機能指向の、またはよりコンパクトな構造内での十分な理由のあるレイヤードアプローチが、明瞭さを高め、認知的負荷を軽減し、より機敏な開発を促進します。
あなたのプロジェクト構造は、あなたのために役立つべきであり、その逆ではありません。

