Go编译器禁止包A与包B相互import,因编译期直接拒绝循环依赖;根本原因是职责混杂,应通过分层(api/service/domain/infrastructure)、接口抽象、依赖注入和物理隔离目录来解耦。

为什么直接 import 包会触发循环依赖报错
Go 编译器禁止包 A 导入包 B 的同时,包 B 又导入包 A。这不是运行时问题,而是编译期直接拒绝——import cycle not allowed。常见于业务层和数据层互相引用:比如 service 包里要调用 repository 接口,但 repository 实现又需要引用 service 里的领域模型或校验逻辑。
真正卡住的不是语法,而是职责混杂:把领域结构体、数据库字段映射、业务规则判断全塞进一个包,自然绕不开相互 import。
- 别把
User结构体定义在model包里再让service和repository都去 import 它——它应该属于领域层,由接口契约约束使用方式 - 避免在
repository实现中调用service的方法(如ValidateUser),校验应下沉到领域对象自身或独立的validator包 - 检查
go list -f '{{join .Deps "\n"}}' ./path/to/pkg输出,确认循环链是否来自中间包(比如utils同时被上下两层 import)
用 interface + 依赖注入切断编译期耦合
关键不是“不 import”,而是“只 import 抽象”。高层模块(如 service)定义接口,低层模块(如 repository)实现它,注入动作发生在启动时,而非编译时。
例如:UserRepository 接口声明在 domain 或 contract 包中,service 依赖该接口,mysql 包实现它并 import domain,但 service 不 import mysql —— 这样依赖方向就单向了。
立即学习“go语言免费学习笔记(深入)”;
-
NewUserService构造函数只接收UserRepository接口类型参数,不关心具体是 MySQL 还是 Mock 实现 - 所有跨层调用必须通过接口,禁止在
service中写mysql.GetUserByID这类硬编码调用 - Wire 工具生成的
injector_gen.go文件里,NewUserService和NewMySQLRepository是两个独立的提供者(Provider),它们之间没有 import 关系
分层目录结构必须强制物理隔离
目录名本身不会解耦,但它是团队对职责边界的共识信号。一旦允许 service/user.go 直接 import data/mysql/user.go,抽象就失效了。
推荐的最小可行分层:
-
api/:仅含 HTTP/gRPC handler,只 importservice和框架(如gin) -
service/:业务编排逻辑,只 importdomain接口和contract类型,不碰任何具体实现 -
domain/:聚合根、值对象、领域服务接口,不 import 任何外部包(包括database/sql) -
infrastructure/:MySQL/Redis/gRPC client 实现,只 importdomain和 SDK,不 importservice
如果发现 service 包里有 import "your/project/infrastructure/mysql",说明分层已破防,必须重构。
第三方 SDK 依赖必须收口到 infrastructure 层
支付 SDK、短信网关、对象存储客户端这些,天然带副作用且难 mock。若散落在各 service 中,单元测试就只能跑集成环境。
- 所有 SDK 调用封装成接口,例如
SMSClient、PaymentGateway,定义在domain或contract - 具体实现(如基于阿里云 SMS SDK 的
aliyunSMS)放在infrastructure/sms/aliyun下,只在这里 import 第三方包 - 业务代码永远只依赖接口,哪怕临时用
log.Print模拟发送,也能跑通单元测试 - 注意 SDK 初始化副作用:不要在
init()函数里初始化 client,而应在 Wire 的 Provider 函数中按需构造
最易被忽略的是领域模型对基础设施的隐式依赖——比如在 User 结构体里加一个 AvatarURL string 字段,看似无害,但可能诱使业务逻辑直接拼接 OSS 地址,绕过 FileStorage 接口。这种细节比循环 import 更难检测,却更伤架构健康度。


















