Go のパッケージ間依存定義をドキュメントから生成する試み
Table of Contents
Goで開発をしているとしばしば遭遇する問題の一つに循環インポート(cycle import)がある。
今回は、あらかじめ import
宣言をしておくことで cycle import を防ぐという発想から、
ドキュメント(Markdown)から import 宣言を生成するツールを Zed の AI Agent で作ったという話。
Go の cycle import 問題 #
Go で適当にパッケージを作っていると cycle import が発生する。 適当に作るのが悪いというのはもっともだが、できればこれを機械的に抑制したい。
また、開発チームに途中からジョインしてアプリケーションの構造に不慣れな場合に、 いちいちパッケージ間の依存関係を調べるのはめんどうだし、 コードレビューで都度間違いを指摘してもらうのも申し訳なさがある。
ドキュメント実装が乖離する問題 #
レイヤー間の依存関係がドキュメント化されていると把握しやすい。 例えばこんなふうに:
# パッケージ間の依存関係について
## レイヤーの定義
このプロジェクトはlayered architectureを採用し、以下の4つのレイヤーを定義しています。
1. ドメイン層
2. アプリケーション層
3. プレゼンテーション層
4. インフラ層
## レイヤーの依存関係
各レイヤーは以下のような依存関係を持ちます。
逆方向の依存は許されません。
インフラ層 → アプリケーション層 → プレゼンテーション層 → ドメイン層
## 各レイヤーに含まれるパッケージとその依存関係
1. ドメイン層
- domain/entity → domain/service
2. アプリケーション層
- usecase → service
3. プレゼンテーション層
- api/route
- cli
4. インフラ層
- database
- cache
- jobqueue
これまたよくある話として、開発が進むにつれてドキュメントと実装が乖離していく問題がある。 ドキュメントを常に最新のコードに追従させるためには努力が必要で、しばしばこの努力は怠られるからだ。 上記の例でいうと、「レイヤーの定義」「各レイヤーに含まれるパッケージ」は早々変わることがないので乖離することはないだろうが、 最後の「各レイヤーに含まれるパッケージ」はパッケージが追加されるたびに更新する必要がある。
理想的には、ドキュメントと実装が常に同期している状態を保ちたい。
import宣言をあらかじめ書いておくという解決策 #
依存しても良いパッケージについて、import _ path/to/package
をあらかじめ書いておくのはどうか、というのが今回の主題。
想定外の依存を記述するとビルドエラー(import cycle not allowed
)が発生するので、すぐに気づくことができる。
上のドキュメントの例でいうと、各パッケージごとに以下のようなファイルをあらかじめ用意しておくことになる:
package database
import (
_ "github.com/handlename/example/api/route"
_ "github.com/handlename/example/cli"
_ "github.com/handlename/example/domain/entity"
_ "github.com/handlename/example/domain/service"
_ "github.com/handlename/example/service"
_ "github.com/handlename/example/usecase"
)
この import
宣言が常にドキュメントに書かれている内容と一致していれば、不要な cycle import は発生しないはずだ。
import宣言をドキュメントから生成する #
これを実現するために、go-package-dependency
というツールを作った。
このツールは、Markdownで定義されたアーキテクチャレイヤー定義に基づいて、各パッケージに dependency.gen.go
ファイルを生成する。
生成されたファイルには、そのパッケージが依存しても良い他のパッケージのimport文が含まれる。
「まずドキュメントを書く」ことをルールとする必要があるが、それさえ守られていればドキュメントとコードとの乖離が発生しない。 ドキュメントはAI Agent に仕事をさせるときのヒントにもなるし、もちろん人間がプロジェクトを理解する助けにもなる。
動作例 #
リポジトリ内の exampleディレクトリ に DEPENDENCY.md
がある。
リポジトリを clone して go run . example/DEPENDENCY.md
とすれば、
example ディレクトリ以下にディレクトリと dependency.gen.go
が生成される。
開発初期の scaffolding としても使えそうだ。
現時点での DEPENDENCY.md
の形式は以下のようになっている:
# Dependencies
## Layers
Upper layers cannot depend on lower layers.
1. Domain layer
- Implementation of core entities
2. Application layer
- Business logic using objects from the domain layer
3. Presentation layer
- UI and presentation logic
4. Infra layer
- Gateway to the real world
## Packages in layers
Upper packages cannot depend on lower packages.
1. Domain layer
- domain/entity
- domain/valueobject
- domain/service
2. Application layer
- app/service
- app/usecase
3. Presentation layer
- api
- cli
4. Infra layer
- infra/database
- infra/cache
この定義に基づいて、各パッケージに適切なimport文を含む dependency.gen.go
が生成される。
❯ tree example/
example/
├── DEPENDENCY.md
├── api
│ └── dependency.gen.go
├── app
│ ├── service
│ │ └── dependency.gen.go
│ └── usecase
│ └── dependency.gen.go
├── cli
│ └── dependency.gen.go
├── domain
│ ├── entity
│ │ └── dependency.gen.go
│ ├── service
│ │ └── dependency.gen.go
│ └── valueobject
│ └── dependency.gen.go
├── go.mod
└── infra
├── cache
│ └── dependency.gen.go
└── database
└── dependency.gen.go
13 directories, 11 files
各 dependency.gen.go
には依存先パッケージの import
宣言が書かれている。
❯ cat example/infra/database/dependency.gen.go
// Code generated by go-package-dependency. DO NOT EDIT.
package database
import (
_ "github.com/handlename/go-package-dependency/example/api"
_ "github.com/handlename/go-package-dependency/example/app/service"
_ "github.com/handlename/go-package-dependency/example/app/usecase"
_ "github.com/handlename/go-package-dependency/example/cli"
_ "github.com/handlename/go-package-dependency/example/domain/entity"
_ "github.com/handlename/go-package-dependency/example/domain/service"
_ "github.com/handlename/go-package-dependency/example/domain/valueobject"
)
AI Agent任せの実装 #
go-package-dependency
は ZedのAI Agent機能 を使って書いた。
思いついたことをとりあえずやってみる分には AI Agent に丸投げで十分だ。
コードもテストも自分では書いていない。
風呂で思いついたアイデアを、関係ない本を読みながら適当に指示を出していたら動くものが完成した。
AI Agent は、Claude Code のプレビュー版が出た頃に比べるとだいぶ賢くなっている。 Zed の Agent Panel を使うことで、複数のファイルにまたがる変更も一度に指示できるようになった。
こういう単発アイデアを「気にはなるけど時間がないからな」とやらずじまいにせず、とりあえず形にできるのはありがたい。
まとめ #
パッケージ間の依存関係をあらかじめ import
宣言で縛っておく。
そのためのコードをドキュメントから生成する、というツールを試しに作ってみた。
Go で何かを書くときは、 自分用のテンプレート を使っている。 フラグ処理やロギングなど、毎回使う何かしらを毎度用意しなくてもいいようにしたものだ。 今回のような設計をドキュメントして残す仕組みを用意することで、 AI Agent 用に最適化していくのがよさそうだ。 今回のツールも、まずは自分で使ってみて、仕組みとして有効そうならコードにしかない要素を検出するなど機能を追加してみるつもりだ。
付録: 同様のアプローチを取った例・ツール #
(by Zed AI Agent)
1. arch-go - アーキテクチャ制約チェックツール #
- GitHub: arch-go/arch-go
- 設定ファイルでパッケージ間の依存関係ルールを定義し、違反をチェック
- 記事の手法に近い「ドキュメント先行でアーキテクチャを定義」するアプローチ
2. go-arch-lint - アーキテクチャリンター #
- GitHub: fe3dback/go-arch-lint
- YAMLファイルでアーキテクチャルールを定義
- hexagonal/onion/DDD/MVCパターンに対応
- CIパイプラインでの使用を想定
3. depguard - インポート許可リスト管理 #
- GitHub: OpenPeeDeeP/depguard
- 許可されたパッケージのリストを定義してインポートをチェック
- 記事の手法に最も近い:事前に依存関係を定義し、違反を検出
// depguardの設定例
rules:
- name: "allowed-packages"
allow:
- "github.com/example/domain"
- "github.com/example/service"
4. layer - レイヤー構造チェッカー #
- GitHub: budougumi0617/layer
- レイヤー構造における不正な依存関係をチェック
- 静的解析でレイヤー越境を検出
付録: 記事手法の独自性 #
(by Zed AI Agent)
調査の結果、記事で紹介されている手法(import _ "package"
を事前に生成してcycle importを予防)は非常にユニークなアプローチです。
類似点:
depguard
:事前に依存関係ルールを定義arch-go
/go-arch-lint
:ドキュメント先行でアーキテクチャを定義
独自点:
- 予防的アプローチ:cycle importが発生する前に防ぐ
- ビルド時検証:コンパイル時に即座にエラーが分かる
- コード生成:ドキュメントから直接実行可能コードを生成
従来の手法は主に「発生したcycle importを解決する」「ルール違反を検出する」ものが多く、「事前にimport宣言を生成して予防する」というアプローチは独特で新しい発想と言えるでしょう。
だそうです。