NamhaeSusan/go-arch-guard
GitHub: NamhaeSusan/go-arch-guard
一款基于 Go test 的架构护栏工具,通过静态分析与预设规则防止架构退化并支持 AI 编码代理。
Stars: 0 | Forks: 0
# go-arch-guard
[](https://github.com/NamhaeSusan/go-arch-guard/actions/workflows/ci.yml)
[](https://codecov.io/gh/NamhaeSusan/go-arch-guard)
[](https://goreportcard.com/report/github.com/NamhaeSusan/go-arch-guard)
[한국어](README.ko.md)
通过 `go test` 为 Go 项目提供架构防护规则,专为 AI 编码代理和快速迭代团队设计。
定义隔离、层方向、结构、命名和影响范围规则,在项目形态偏离时让常规测试失败。内置 **DDD**、**Clean Architecture**、**Layered**、**Hexagonal**、**Modular Monolith**、**Consumer/Worker**、**Batch** 和 **Event-Driven Pipeline** 预设,支持完全自定义架构模型。无需学习 CLI,无需单独配置文件。只需 Go 测试。
对 AI 代理友好:
- `scaffold.ArchitectureTest(...)` 生成可直接复制的 `architecture_test.go`
- `rules.RunAll(...)` 一次性运行推荐的规则集合
- `report.MarshalJSONReport(...)` 输出机器可读的违规记录,供机器人和修复循环使用
## 为什么
架构退化通常源于少数几类广泛错误,而非深层的理论违规:
- 跨域导入
- 隐藏的组成根
- 包位置漂移
- 破坏预期项目形态的命名
`go-arch-guard` 通过静态分析尽早捕获这些粗粒度错误。它设计得足够简单,便于 AI 代理生成和维护,同时仍对人类审查生成的边界有用。它不会尝试建模 Go 包内部的每个语义细节,如果 Go 自身已拒绝某些内容(如导入循环),那也不是主要目标。
## 安装
```
go get github.com/NamhaeSusan/go-arch-guard
```
## 快速开始
### 生成预设模板
对于 AI 代理或脚手架工具,生成可直接复制的 `architecture_test.go`:
```
import "github.com/NamhaeSusan/go-arch-guard/scaffold"
src, err := scaffold.ArchitectureTest(
scaffold.PresetHexagonal,
scaffold.ArchitectureTestOptions{PackageName: "myapp_test"},
)
```
`PackageName` 必须是有效的 Go 包标识符。不要盲目从连字符化的模块基名派生。
可用预设:`PresetDDD`、`PresetCleanArch`、`PresetLayered`、
`PresetHexagonal`、`PresetModularMonolith`、`PresetConsumerWorker`、`PresetBatch`、
`PresetEventPipeline`。
### 推荐快捷方式
如果你想要推荐的规则集合而无需手动追加每个检查:
```
violations := rules.RunAll(pkgs, "", "")
report.AssertNoViolations(t, violations)
```
仅在需要非默认模型或严重性/排除选项时传递 `opts...`。
### 每规则控制(DDD 示例)
对于单个检查的细粒度控制,手动组合它们:
```
func TestArchitecture(t *testing.T) {
pkgs, err := analyzer.Load(".", "internal/...", "cmd/...")
if err != nil {
t.Log(err)
}
if len(pkgs) == 0 {
t.Fatalf("no packages loaded: %v", err)
}
t.Run("domain isolation", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckDomainIsolation(pkgs, "", ""))
})
t.Run("layer direction", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckLayerDirection(pkgs, "", ""))
})
t.Run("naming", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckNaming(pkgs))
})
t.Run("structure", func(t *testing.T) {
report.AssertNoViolations(t, rules.CheckStructure("."))
})
t.Run("blast radius", func(t *testing.T) {
report.AssertNoViolations(t, rules.AnalyzeBlastRadius(pkgs, "", ""))
})
}
```
对于其他预设,使用对应的模型函数传入 `opts`:
```
m := rules.CleanArch() // or Layered(), Hexagonal(), ModularMonolith(), ConsumerWorker(), Batch(), EventPipeline()
opts := []rules.Option{rules.WithModel(m)}
rules.CheckDomainIsolation(pkgs, "", "", opts...)
rules.CheckLayerDirection(pkgs, "", "", opts...)
// ... same pattern for all Check* functions
```
### 自定义模型
```
m := rules.NewModel(
rules.WithDomainDir("module"),
rules.WithSharedDir("lib"),
rules.WithSublayers([]string{"api", "logic", "data"}),
rules.WithDirection(map[string][]string{
"api": {"logic"},
"logic": {"data"},
"data": {},
}),
)
opts := []rules.Option{rules.WithModel(m)}
```
运行:
```
go test -run TestArchitecture -v
```
存在违规时的示例输出:
```
=== RUN TestArchitecture/domain_isolation
[ERROR] violation: domain "order" must not import domain "user"
(file: internal/domain/order/app/service.go:5,
rule: isolation.cross-domain,
fix: use orchestration/ for cross-domain orchestration or move shared types to pkg/)
--- FAIL: TestArchitecture/domain_isolation
```
传入空字符串作为 `module` 和 `root` 可自动从已加载包中提取。如果无法确定模块,将发出 `meta.no-matching-packages` 警告。
## 预设
| 预设 | 类型 | 子层 | 方向 |
|------|------|------|------|
| `DDD()` | 领域 | handler, app, core/model, core/repo, core/svc, event, infra | handler→app→core/*, infra→core/repo+core/model+event |
| `CleanArch()` | 领域 | handler, usecase, entity, gateway, infra | handler→usecase→entity+gateway, infra→gateway+entity |
| `Layered()` | 领域 | handler, service, repository, model | handler→service→repository+model |
| `Hexagonal()` | 领域 | handler, usecase, port, domain, adapter | handler→usecase→port+domain, adapter→port+domain |
| `ModularMonolith()` | 领域 | api, application, core, infrastructure | api→application→core, infrastructure→core |
| `ConsumerWorker()` | 平铺 | worker, service, store, model | worker→service+model, service→store+model, store→model |
| `Batch()` | 平铺 | job, service, store, model | job→service+model, service→store+model, store→model |
| `EventPipeline()` | 平铺 | command, aggregate, event, projection, eventstore, readstore, model | command→aggregate+eventstore+model, aggregate→event+model, projection→event+readstore+model |
领域预设使用 `internal/domain/{name}/{layer}/` 布局。
平铺预设使用 `internal/{layer}/` 布局(无 domain 目录)。
详见 [预设详情](docs/presets.md) 获取完整的布局示意图和方向表。
### 自定义模型选项
从 DDD 默认值开始,按需覆盖:
```
m := rules.NewModel(
rules.WithDomainDir("module"), // internal/module/ instead of internal/domain/
rules.WithOrchestrationDir("workflow"), // internal/workflow/
rules.WithSharedDir("lib"), // internal/lib/
rules.WithSublayers([]string{"api", "logic", "data"}),
rules.WithDirection(map[string][]string{
"api": {"logic"},
"logic": {"data"},
"data": {},
}),
rules.WithRequireAlias(false),
rules.WithRequireModel(false),
)
```
所有模型选项:
| 选项 | 描述 |
|------|------|
| `WithSublayers([]string{...})` | 识别的子层名称 |
| `WithDirection(map[string][]string{...})` | 允许的导入方向矩阵 |
| `WithPkgRestricted(map[string]bool{...})` | 必须不导入共享 pkg 的子层 |
| `WithDomainDir("domain")` | 域顶级目录名称 |
| `WithOrchestrationDir("orchestration")` | 编排顶级目录名称 |
| `WithSharedDir("pkg")` | 共享包顶级目录名称 |
| `WithRequireAlias(bool)` | 域根是否必须定义 alias.go |
| `WithAliasFileName("alias.go")` | 别名文件名 |
| `WithRequireModel(bool)` | 域是否必须包含 model 目录 |
| `WithModelPath("core/model")` | 域 model 目录路径 |
| `WithDTOAllowedLayers([]string{...})` | 允许 DTO 的层 |
| `WithBannedPkgNames([]string{...})` | internal/ 下禁止的包名 |
| `WithLegacyPkgNames([]string{...})` | 触发迁移警告的包名 |
| `WithLayerDirNames(map[string]bool{...})` | 被视为“类似层”的目录名 |
| `WithInterfacePatternExclude(map[string]bool{...})` | 跳过接口模式检查的层 |
## 隔离规则
`rules.CheckDomainIsolation(pkgs, module, root, opts...)`
防止域相互渗透。若无隔离,域 A 的变更可能无声地破坏域 B —— 这是 DDD 项目中最常见的意外耦合来源。
### `isolation.cross-domain`
域不得直接导入其他域。
```
// internal/domain/order/app/service.go
package app
import _ "myapp/internal/domain/user/app" // violation
```
```
// use orchestration for cross-domain coordination
package orchestration
import (
"myapp/internal/domain/order"
"myapp/internal/domain/user"
)
```
### `isolation.cmd-deep-import`
`cmd/` 仅能导入域根别名包,不得导入子包。
```
// cmd/server/main.go
import _ "myapp/internal/domain/order/app" // too deep
import _ "myapp/internal/domain/order" // domain root only
```
### `isolation.orchestration-deep-import`
编排仅能导入域根,保持耦合面最小。
```
// internal/orchestration/checkout.go
import _ "myapp/internal/domain/order/app" // too deep
import _ "myapp/internal/domain/order" // domain root only
```
### `isolation.pkg-imports-domain`
共享 `pkg/` 不得导入任何域 —— 应该是与域无关的。
```
// internal/pkg/logger/logger.go
import _ "myapp/internal/domain/order" // violation: pkg depends on domain
```
### `isolation.pkg-imports-orchestration`
共享 `pkg/` 不得导入编排。
### `isolation.domain-imports-orchestration`
域不得导入编排 —— 编排协调域,而非反过来。
### `isolation.stray-imports-orchestration`
仅 `cmd/` 和编排本身可以依赖编排。
### `isolation.stray-imports-domain`
非域内部包(除编排/cmd/pkg 外)不得导入域。
**导入矩阵:**
| from | 域根 | 域子包 | 编排 | 共享 pkg |
|------|------|--------|------|----------|
| **同域** | 是 | 是 | 否 | 是 |
| **其他域** | 否 | 否 | 否 | 是 |
| **编排** | 是 | 否 | 是 | 是 |
| **cmd** | 是 | 否 | 是 | 是 |
| **共享 pkg** | 否 | 否 | 否 | 是 |
## 层方向规则
`rules.CheckLayerDirection(pkgs, module, root, opts...)`
防止层之间的反向依赖。若不强制方向,内部层(model、entity)会逐渐从外层累积导入,使其无法独立提取或测试。
### `layer.direction`
导入必须遵循预设方向矩阵定义的方向。
```
// DDD preset: core/svc may only import core/model
package svc // internal/domain/order/core/svc/
import _ "myapp/internal/domain/order/app" // reverse direction
import _ "myapp/internal/domain/order/core/model" // allowed
```
### `layer.inner-imports-pkg`
标记为 `PkgRestricted` 的内层不得导入共享 `pkg/`。
这确保核心域逻辑不包含基础设施关注点。
### `layer.unknown-sublayer`
检测域下不符合任何识别子层名称的目录。
```
internal/domain/order/utils/ "utils" is not a recognized sublayer
```
## 结构规则
`rules.CheckStructure(root, opts...)`
强制执行文件系统布局约定,防止在 vibe 编码过程中结构漂移。
### `structure.internal-top-level`
仅允许指定目录存在于 `internal/` 顶层。
```
// DDD: only domain/, orchestration/, pkg/ allowed
internal/
domain/ allowed
orchestration/ allowed
pkg/ allowed
config/ not in allowed list
```
### `structure.banned-package`
阻止模糊的包名,这些名称会成为倾倒场。
默认禁止:`util`、`common`、`misc`、`helper`、`shared`、`services`
### `structure.legacy-package`
警告应迁移的包名:`router`、`bootstrap`
### `structure.misplaced-layer`
层目录(`app`、`handler`、`infra`)只能存在于域切片内,不能漂浮在 `internal/` 顶层。
### `structure.middleware-placement`
`middleware/` 必须位于 `internal/pkg/middleware/`,不得分散在域中。
### `structure.domain-alias-exists`(DDD 仅)
每个域根必须定义 `alias.go` 文件作为其公共 API 表面。
### `structure.domain-alias-package`
别名文件的包名必须与目录名匹配。
### `structure.domain-alias-exclusive`
域根目录只能包含 `alias.go` —— 所有其他代码应位于子层。
### `structure.domain-alias-no-interface`
别名文件不得直接定义接口 —— 这会泄露跨域契约。
### `structure.domain-alias-contract-reexport`
别名文件不得重新导出子层(repo/svc)的类型 —— 这会在跨域创建隐藏依赖。
### `structure.domain-model-required`(DDD 仅)
每个域必须包含 `core/model/` 目录,且至少有一个 Go 文件。
### `structure.dto-placement`
DTO 文件(`dto.go`、`*_dto.go`)只能存在于允许的层(handler、app)。
## 命名规则
`rules.CheckNaming(pkgs, opts...)`
强制执行 Go 命名约定,保持代码库一致且便于 grep。
### `naming.no-stutter`
导出的类型不得重复包名。
```
package repo
type RepoOrder struct{} // stutters: repo.RepoOrder
type Order struct{} // clean: repo.Order
```
### `naming.no-impl-suffix`
导出的类型不得以 `Impl` 结尾。请使用未导出类型替代。
```
type OrderServiceImpl struct{} // Impl suffix
type orderService struct{} // unexported
```
### `naming.snake-case-file`
所有 Go 文件名必须为蛇形命名(snake_case)。
```
OrderService.go violation
order_service.go correct
```
### `structure.repo-file-interface`
`repo/`(或 `core/repo/`)中的文件必须包含与文件名匹配的接口。
```
// order.go in repo/ must define:
type Order interface { ... } // matches filename
```
### `structure.repo-file-extra-interface`
`repo/` 中的每个文件必须只定义一个接口。额外接口应拆分到独立文件。
```
// repo/review.go
type Review interface { Find() } // correct
type Helper interface { Assist() } // violation: move to helper.go
```
### `interface.too-many-methods`
repo 接口不得超出 `WithMaxRepoInterfaceMethods` 设置的限制。默认禁用。
```
rules.CheckNaming(pkgs, rules.WithMaxRepoInterfaceMethods(10))
```
```
// repo/review.go
type Review interface {
// 11 methods --- violation (max 10)
}
```
### `naming.no-layer-suffix`
文件名不得冗余地重复层名。
```
// inside service/ directory:
order_service.go "_service" suffix is redundant
order.go correct
```
### `structure.interface-placement`(DDD 仅)
仓库端口接口(名称以 `Repository` 或 `Repo` 结尾)必须定义在 `core/repo/`,不得分散在多层。消费者定义的接口(Go 惯用法,即包声明其使用的最小接口)可放置在任意位置:`handler/`、`app/`、`svc/` 等。
同时标记 `type X = otherdomain.Repo` 的别名(跨域重新导出仓库接口)应位于 `orchestration/`。
### `testing.no-handmock`
测试文件不得定义手工编写的 mock/fake/stub 结构体及其方法。应使用 mockery 或其他生成工具。
### `naming.type-pattern-mismatch`(平铺预设)
匹配 TypePattern 前缀的文件必须定义对应的类型。
```
// worker/worker_order.go must define:
type OrderWorker struct{} // expected
type SomethingElse struct{} // expected OrderWorker
```
### `naming.type-pattern-missing-method`(平铺预设)
匹配 TypePattern 的类型必须包含所需方法。
```
type OrderWorker struct{}
// missing Process method --- violation
func (w *OrderWorker) Process(ctx context.Context) error { ... } // correct
```
## 接口模式规则
`rules.CheckInterfacePattern(pkgs, opts...)`
强制执行 Go 接口最佳实践:私有实现、仅 `New()` 构造函数、接口返回类型、每个包一个接口。
### `interface.exported-impl`
导出的结构体不得实现接口 —— 应将实现类型设为未导出,以防止消费者依赖具体类型。
```
type RepositoryImpl struct{ db *sql.DB } // exported struct implements interface
type repository struct{ db *sql.DB } // unexported --- correct
```
### `interface.constructor-name`
构造函数必须命名为 `New`,而非 `NewXxx` 变体。这在所有包中强制一致的工厂模式。
```
func NewRepository(db *sql.DB) Repository // NewXxx not allowed
func New(db *sql.DB) Repository // correct
```
### `interface.constructor-returns-interface`
`New()` 必须返回接口,而非具体类型。这确保调用方依赖契约而非实现。
```
func New(db *sql.DB) *repository // returns concrete type
func New(db *sql.DB) Repository // returns interface --- correct
```
### `interface.single-per-package`
每个包最多一个导出接口(警告)。单个包中存在多个接口通常意味着职责过多。
排除层由 `InterfacePatternExclude` 按预设控制(入口点、model、event、pkg)。
### `interface.cross-domain-anonymous`
检测在所属域之外(且不在指定编排层)声明的匿名接口,其方法签名触及其他域的类型。默认严重级别为 **Error**。
该规则强制约定:**跨域抽象应由编排包拥有**,而非任意布线代码。
在 `cmd/`(或 `internal/pkg/`)中声明与域类型相关的匿名接口会创建不受控的跨域表面;此类适配器/抽象应位于 `internal/orchestration/`。
```
// flagged: cmd/ declares inline interface that abstracts a domain type
package main
import "example.com/p/internal/domain/user"
type adapter struct {
repo interface { // ← cross-domain anonymous in cmd/
GetByID(ctx context.Context, id string) (*user.User, error)
}
}
```
```
// not flagged: same shape but inside the orchestration layer where
// cross-domain coordination is by design
package orchestration
import "example.com/p/internal/domain/user"
type userInfoAdapter struct {
repo interface { // ← anonymous, but orchestration is exempt
GetByID(ctx context.Context, id string) (*user.User, error)
}
}
```
修复方法是 **将适配器移至编排包**,并让布线代码调用编排构造函数,而非自行声明接口。
跳过:
- 测试文件(`_test.go`),其中 mock/fake 固件自然采用此形态
- 空接口(`interface{}`)及无方法声明的接口
- 嵌入接口类型(如 `interface { io.Reader }`)
- 同域引用(`internal/domain/X` 中匿名接口引用同域类型)
- `internal//` 包中的类型 —— 编排是指定的跨域协调层
- 无 domainDir 的模型(平铺布局如 ConsumerWorker、Batch、EventPipeline)
### `interface.container-only`
检测到接口仅在结构体字段类型中使用 —— 从未作为函数参数或返回类型。默认严重级别为 **Warning**。
这是 vibe 编码的异味:接口被用作值容器而非抽象。常见原因是布线层需要持有某个值,而其具体类型未暴露(例如 `alias.go` 重新导出构造函数但未导出类型),于是开发者声明本地接口仅为字段提供类型。
```
// flagged: container-only — never used as parameter or return
type userRepo interface {
GetByID(id string) string
}
type holder struct {
r userRepo // only usage
}
```
```
// not flagged: legitimate consumer-defined interface
type userRepo interface {
GetByID(id string) string
}
func newHolder(r userRepo) *holder { // used as parameter → real abstraction
return &holder{r: r}
}
```
跳过:
- 测试文件(`_test.go`),其中 mock/fake 固件自然采用此形态
- 类型别名(`type Foo = pkg.Foo`)
- 结构体中的嵌入字段(匿名嵌入)
- 未使用的接口(属于不同类别)
该规则不规定修复方式。两种常见解决:
1. 从 `alias.go` 重新导出具体类型,使字段可直接持有该类型。
2. 重写布线逻辑,使值成为函数内的局部变量,而非跨函数共享的结构体字段。
严重性可通过 `WithSeverity(Error)` 升级为 Error,若项目希望将此异味作为硬规则。
## 影响范围分析
`rules.AnalyzeBlastRadius(pkgs, module, root, opts...)`
通过 IQR 统计离群值检测内部包的高耦合度。默认严重级别为 Warning。跳过包含少于 5 个内部包的项目
| 规则 | 含义 |
|------|------|
| `blast.high-coupling` | 包具有统计上显著的传递依赖数 |
| 指标 | 定义 |
|------|------|
| Ca(入度耦合) | 导入该包的其他包数量 |
| Ce(出度耦合) | 该包导入的其他包数量 |
| 不稳定性 | Ce / (Ca + Ce) |
| 传递依赖 | 通过 BFS 反向可达的完整集合 |
## 选项
### 严重性
```
// Log violations without failing the test
rules.CheckDomainIsolation(pkgs, "", "", rules.WithSeverity(rules.Warning))
```
### 排除路径
```
// Skip subtrees during migration
rules.CheckDomainIsolation(pkgs, "", "",
rules.WithExclude("internal/legacy/..."),
)
```
模式为项目相对路径,使用正斜杠。`...` 匹配根目录及其所有后代。
## TUI 查看器
在终端 UI 中可视化项目包结构与依赖关系。
```
go run github.com/NamhaeSusan/go-arch-guard/cmd/tui .
```
特性:健康状态树着色、导入/反向依赖/耦合指标、违规详情、搜索/过滤(`/`)、键盘导航。
## API 参考
| 函数 | 描述 |
|------|------|
| `analyzer.Load(dir, patterns...)` | 加载 Go 包用于分析 |
| `rules.CheckDomainIsolation(pkgs, module, root, opts...)` | 跨域边界检查 |
| `rules.CheckLayerDirection(pkgs, module, root, opts...)` | 域内方向检查 |
| `rules.CheckNaming(pkgs, opts...)` | 命名约定检查 |
| `rules.CheckStructure(root, opts...)` | 文件系统结构检查 |
| `rules.AnalyzeBlastRadius(pkgs, module, root, opts...)` | 耦合离群检测 |
| `rules.CheckInterfacePattern(pkgs, opts...)` | 接口模式最佳实践 |
| `rules.RunAll(pkgs, module, root, opts...)` | 运行内置推荐规则集合 |
| `report.AssertNoViolations(t, violations)` | 在测试中因 Error 违规失败 |
| `report.BuildJSONReport(violations)` | 构建机器可读的 JSON 报告 |
| `report.MarshalJSONReport(violations)` | 序列化机器可读的 JSON 报告 |
| `report.WriteJSONReport(w, violations)` | 写入机器可读的 JSON 报告 |
| `scaffold.ArchitectureTest(preset, opts)` | 生成预设专用的 `architecture_test.go` 模板 |
| `rules.DDD()` | DDD 架构模型(默认) |
| `rules.CleanArch()` | Clean Architecture 模型 |
| `rules.Layered()` | Spring 风格的分层模型 |
| `rules.Hexagonal()` | 端口与适配器模型 |
| `rules.ModularMonolith()` | 基于模块的层次模型 |
| `rules.ConsumerWorker()` | Consumer/Worker 平铺布局模型 |
| `rules.Batch()` | Batch 平铺布局模型 |
| `rules.EventPipeline()` | 事件溯源 / CQRS 平铺布局模型 |
| `rules.CheckTypePatterns(pkgs, opts...)` | 基于 AST 的类型模式强制 |
| `rules.NewModel(opts...)` | 自定义模型构建器 |
| `rules.WithModel(m)` | 将自定义模型应用于检查 |
| `rules.WithSeverity(rules.Warning)` | 降级为 Warning |
| `rules.WithExclude("path/...")` | 跳过子树 |
| `rules.WithMaxRepoInterfaceMethods(10)` | 限制 repo 接口方法数量 |
## 机器可读的 JSON 输出
用于 CI、机器人或自动化修复循环,以相同格式输出违规记录:
```
import "github.com/NamhaeSusan/go-arch-guard/report"
data, err := report.MarshalJSONReport(violations)
if err != nil {
return err
}
fmt.Println(string(data))
```
## Claude Code 插件
```
/plugin marketplace add NamhaeSusan/go-arch-guard
/plugin install go-arch-guard@go-arch-guard-marketplace
```
## 外部导入卫生
`go-arch-guard` 仅检查**项目内部**导入。外部依赖的卫生应由 AI 指令和代码审查保证。参考 [DDD 外部导入约束](README.ko.md#외부-import-위생--이-라이브러리가-아닌-ai-도구-지침으로-강제) 获取可复制的模板。
## 许可证
MIT
标签:AI编码代理, CI集成, DDD, EVTX分析, Go, Go测试, Ruby工具, 事件驱动流水线, 云安全监控, 代码库结构, 代码规范, 六角架构, 包隔离, 命名规范, 批处理, 文档结构分析, 日志审计, 架构分层, 架构守护, 架构规则, 模块化单体, 测试驱动, 消费者工作器, 清洁架构, 静态分析