Go单体项目模块化核心是职责边界、显式依赖与接口设计,而非过早物理拆分;应先逻辑分层(domain/application/infrastructure等)、包级封装、接口+DI解耦,再按需渐进升级为多module。

单体工程直接改多模块不是“拆包”就能成,Go 的 go.mod 模块边界和 import 路径强绑定,硬拆会导致循环依赖、import cycle not allowed、测试无法运行、vendor 失效等连锁问题。
为什么 go mod init 后加子模块总报 import cycle not allowed
根本原因是 Go 模块系统不支持“模块内嵌套模块”这种目录结构。比如你在 myapp/ 下执行 go mod init myapp,再在 myapp/domain/ 里执行 go mod init myapp/domain,Go 会认为这是两个独立模块——但 myapp 仍试图 import myapp/domain,而后者又可能反向 import myapp 的工具函数或 config,立刻触发循环。
- 正确做法:整个项目只有一个
go.mod,放在根目录;子目录(如domain、infra、app)只是包(package),不是模块(module) - 模块名必须全局唯一且稳定,不能随目录改;例如
myapp就是模块名,myapp/domain是导入路径,不是新模块 - 如果真要物理隔离(比如 domain 层要单独发版),得把
domain提到独立 Git 仓库,并用replace或require引入,而不是本地子目录
领域层(domain)怎么设计才不会被 infra 或 app 层污染
Domain 包必须是纯 Go 结构体 + 接口,不含任何外部依赖。一旦你看到 domain.User 里有 gorm.Model 字段,或者方法里调用了 logrus、redis.Client,说明边界已经破了。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 接口定义写在
domain/,实现写在infra/;例如domain.UserRepository接口在 domain 包,infra/mysql/user_repo.go实现它 - 所有跨层数据传递用 domain 类型,禁止传
*sql.Rows、map[string]interface{}、gin.Context这类 infra/app 层类型 - domain 包的
go.mod里不能出现任何第三方 import,go list -f '{{.Deps}}' domain应该只返回标准库
如何验证模块解耦是否真正生效
别只看目录结构,重点看三件事:编译能否断开、测试能否隔离、依赖图是否单向。
立即学习“go语言免费学习笔记(深入)”;
- 删掉
infra/目录后,go build ./domain/...必须成功;失败说明 domain 层偷偷引用了 infra 的东西 - 运行
go test ./domain/...时不能依赖config.yaml或数据库连接,否则 domain 不够“纯” - 用
go mod graph | grep 'myapp/'查依赖流向:应只有myapp/app → myapp/domain、myapp/infra → myapp/domain,绝不能出现反向箭头
真正卡住人的从来不是语法或命令,而是“以为拆了目录就解耦了”的错觉。domain 包里一个 time.Now() 调用看似无害,但一旦 infra 层想 mock 时间做测试,就得把 domain 改成接收 clock.Clock 接口——这个动作本身,才是解耦开始的地方。

















