1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Go: プロジェクトレイアウトの再検討

Posted at

概要

アドベントカレンダーの他の日でも書きましたが、システムに関する情報を一か所(ここではレポジトリ)に集約したいという要望が生成AI活用の観点他から出てきました。

そこでまずは(業務では少数派ですが)自分の関心のある Go を対象に再度プロジェクトレイアウトについて確認・検討してみます。

前提

本記事のかなりの個所は以下の記事

の内容を参考にしています。必要に応じてカッコ内の名称で言及していきます。

Go 単独の場合

方針

  • testdata/ : 使うがこれからのディレクトリレイアウトには記載しない
    • テストデータ結局アプリケーションごとに必要だと思うので、 go.mod と同じ階層にそれぞれ作る形になる想定です
  • internal/ : 使わない
    • 今回は扱うプログラムを Web アプリや CLI アプリに絞って考えます。そのためFA記事にもあるように「ライブラリの公開を目的としていて、内部 API を使われたくない」ケースでもないので internal/ ディレクトリは使わないことにします
  • docs/ : 代わりに docsrc/ を使う
    • GitHub 前提なので、GitHub Pages 利用の可能性があるディレクトリを避けるため
    • 配置位置はアプリケーションと同列でしょうか

フラットなパターン (Go公式記事の「Basic command」、FA記事の「最小構成」)

小規模(私が作ってる大半のものがそうですが)であれば以下のレイアウトで充分かと思います。

/project-root
├── cmd/
│   ├── app-backend/
│   │       └── main.go # APIサーバーのエンドポイント
│   └── admin-cli/
│           └── main.go # 管理用CLIのエンドポイント
├── handlers.go
├── models.go
├── database.go
├── const.go
├── go.mod
└── README.md           # module project-root

※ 各 *.go と同列にテストコードの *_test.go がありますが、省略しています

main.go 以外のファイル数が多かったり、1ファイルのサイズが大きいのは Go 的にはナシではないパターンなので、結構これで行けたりします。FA記事でも言及がありますが、Goの os モジュールも

アーキテクチャ違いとテストファイル込みですが、1ディレクトリ配下に100以上のファイル配置してたりしますし。

ここから一段難しくなるので一旦整理

Go はディレクトリ=モジュールになるので、ディレクトリを分けるとモジュールが別になります。モジュールは循環参照ができないので、共通コード・共通定義のブリッジについて考慮が必要になります。

FA記事の案をまとめると以下でしょうか。

メリット デメリット
1. common的なパッケージを作る - 循環参照の解消に直感的に使える
- 共通的な定義を他パッケージから扱いやすい
- 「common」「util」等の曖昧な名前になりがち
- 何でも入ってしまい肥大化・無秩序化しやすい
2. ルートのロジックを廃していく - サブフォルダへ明確に責務分担できる
- main.go以外、ディレクトリ構成がスッキリ
- main.goが肥大化しやすい(mainへの初期化/差し込みコードが増える)
3. ルートの定数や共通のものを末端パッケージに移動する - 疎なパッケージ構成にでき独立利用やOSS化も視野に入る
- 明確な責務分割ができる
- パッケージ増加時に定数似通り・変換が必要になることがある
- 末端に共通的な「基礎定義」が溜まりやすくなる
4. 抽象と具象で階層化する - 明確な依存方向の設計となり大規模開発でメリット大
- 標準ライブラリや拡張性重視設計と親和性高い
- 小規模や中規模プロジェクトには過剰設計になることも
- 抽象・具象の切り分け設計コストが必要

5年くらい前に対応した時は「案2. ルートのロジックを排していく」に近い対応をして、 main.go の初期化コードがかなり長くなりました。案3、案4がうまくできるといいのですが、当時はあまりうまくできなかったですね。

案1は /project-root 配下に共通参照の common/ を作成するのでディレクトリ構成に反映するのも分かりやすく、案2, 案3は定義配置位置の差になり、案4は Go のソースコードの path の構造を参照してもらうとして、おおむね以下のイメージになるかと思います。

/project-root
├── cmd/
│   ├── app-backend/
│   │       └── main.go # APIサーバーのエンドポイント
│   └── admin-cli/
│           └── main.go # 管理用CLIのエンドポイント
├── common/             # 案1だと作る。各モジュールから参照するものはここに集約
│   └── const.go
├── handlers/
│   └── handlers.go
├── models/
│   └── models.go
├── database/
│   └── database.go
├── go.mod
└── README.md           # module project-root

モノレポ(複数アプリケーション同一レポジトリ配置)の場合

最終的な検討の内容に「同一システム(群)は1つのレポジトリに収めたい」という要望があります。

モノレポのディレクトリ構成のような内容で調べると、出てくるのは主にフロントエンド向けのツール(中身詳細まで確認出来ていないですが、NxTurborepo など)の推奨構成のようでした。

以下のようなイメージです。

/monorepo-root
├── apps/               # 実行可能なアプリケーションやサービス
│   ├── backend/        # APIサーバー
│   ├── frontend/       # Web UI
│   └── admin-cli/      # 管理CLIツール
├── libs/               # 再利用可能なライブラリやパッケージ(他の名前の候補としては packages/ など)
│   ├── ui/             # フロントエンドの共有UIコンポーネント
│   ├── proto/          # 共有 protobuf 定義
│   └── shared-XX-lib/  # 言語ごとの共有ライブラリ
├── tools/              # ビルドやCI用のスクリプト
└── (monorepo-conf)     # monorepo・用ツール設定ファイル

この構成だと libs/shared-XX-lib/ が Go 単独の場合の「案1. common的なパッケージを作る」になりますね。これ以上は後日モノレポの検討をするときに譲りたいと思いますが、最後に Go 固有の管理についての話を書いておきます。

例えば libs/shared-go-lib/ だったとするとモジュールの管理に

  • go.mod: apps/backendlib/shared-go-lib のモジュール管理
  • go.work: go.mod の管理用。開発者がローカルで使うもの。レポジトリにはコミットしない

を設定して以下のようなレイアウトになると思います。

/monorepo-root
├── apps/               # 実行可能なアプリケーションやサービス
│   ├── backend/        # APIサーバー
│   │   └── go.mod      # apps/backend のモジュール管理ファイル
│   ├── frontend/       # Web UI
│   └── admin-cli/      # 管理CLIツール
├── libs/               # 再利用可能なライブラリやパッケージ(他の名前の候補としては packages/ など)
│   ├── ui/             # フロントエンドの共有UIコンポーネント
│   ├── proto/          # 共有 protobuf 定義
│   └── shared-go-lib/  # 言語ごとの共有ライブラリ
│       └── go.mod      # lib/shared-go-lib のモジュール管理ファイル
├── tools/              # ビルドやCI用のスクリプト
├── go.work             # 複数個所に go.mod があるので管理用に
├── .gitignore          # go.work は除外に入れる
└── (monorepo-conf)     # monorepo・用ツール設定ファイル

go.work をなぜレポジトリにコミットしないかは Go 公式の以下に記載があります。

It is generally inadvisable to commit go.work files into version control systems, for two reasons:

  • A checked-in go.work file might override a developer’s own go.work file from a parent directory, causing confusion when their use directives don’t apply.
  • A checked-in go.work file may cause a continuous integration (CI) system to select and thus test the wrong versions of a module’s dependencies. CI systems should generally not be allowed to use the go.work file so that they can test the behavior of the module as it would be used when required by other modules, where a go.work file within the module has no effect.

That said, there are some cases where committing a go.work file makes sense. For example, when the modules in a repository are developed exclusively with each other but not together with external modules, there may not be a reason the developer would want to use a different combination of modules in a workspace. In that case, the module author should ensure the individual modules are tested and released properly.

https://go.dev/ref/mod#workspaces

おわりに

Go 単独だと諸々の検討ポイントがあったレイアウトですが、他のアプリケーション(他の言語で作成されたものも含む)と同居する場合にはざっと調べた感じでは common/ を作ることになりそうでした。

この点に関しては別の機会でまた再度検討したいと思います。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?