接口必须物理隔离于独立包(如api或contract),禁止与实现共存同一包,否则无法真正解耦;跨域调用须通过client包或事件,严禁直接导入internal实现;共享类型应置于独立shared/model模块,且契约演化需受控。

接口定义必须放在独立包里,否则无法真正解耦
很多团队把 UserRepository 接口和 MySQL 实现都塞进 repository 包,结果测试时只能 mock 整个包,或干脆绕过接口直接调用实现——这等于没解耦。接口的唯一作用是提供可替换契约,它必须物理隔离。
- 接口应放在
api或contract子包中(如github.com/org/user-service/api),路径即契约归属,不依赖包名 - 实现包(如
impl)不能 import 接口所在包以外的业务逻辑包,否则会引入隐式依赖 - 避免在接口定义里引用具体类型:比如
type UserRepository interface { GetUser(ctx context.Context, id int64) (*User, error) }中的*User应来自model包,而该包必须是纯数据结构、无逻辑、无依赖
internal 目录不是“保险箱”,滥用会导致模块边界失效
internal 是 Go 的语言级封装机制,但它只拦得住跨 module 导入,拦不住同一 module 内部的乱引。常见错误是把所有 service 都扔进 internal/order,再让 internal/payment 直接 import internal/order/service —— 这和没分层没区别。
- 每个业务域(如 order、user、payment)应为独立子 module,或至少用
internal/{domain}/+ 显式禁止规则(如//go:build !unit配合构建标签)约束跨域引用 -
internal下仍需分层:推荐internal/{domain}/handler、internal/{domain}/service、internal/{domain}/repository,且handler只能依赖同域service,service只能依赖同域repository和其他域的api接口 - 跨域调用必须走 client 包(如
client/user)或消息事件,禁止直接 import 对方 internal 实现
Wire 生成的注入代码容易掩盖循环依赖,别跳过手动检查
Wire 能自动生成 NewApp() 函数,但若两个 service 互相持有对方接口(A 依赖 B 的接口,B 的实现又悄悄 new 了 A),Wire 默认不会报错,直到运行时报 panic: runtime error: invalid memory address 或死锁。
- 在
wire.go中显式声明依赖方向:用wire.Build(...)列出所有 provider,并确保没有 provider 函数内部 new 其他 service 实例 - 所有构造函数参数必须是接口,禁止传
*sql.DB或redis.Client等具体类型——它们应由 provider 封装后暴露为接口(如Datastore) - 生成后检查
injector_gen.go:如果看到类似new(OrderService)出现在NewPaymentService的初始化链路中,说明有隐式强依赖,得重构
模块命名不是越细越好,语义冲突比重复更致命
有人把每个服务拆成 user-api、user-impl、user-client 三个独立 module,结果在主应用里 import 时写一堆别名:uapi "github.com/x/user-api"、uimpl "github.com/x/user-impl"……最后发现 uapi.User 和 uimpl.User 类型不兼容,因为它们根本不是同一个 struct。
立即学习“go语言免费学习笔记(深入)”;
- 一个服务的所有子包(
api、impl、client)应属于同一个 module(如github.com/org/user-service),靠目录区分职责,而非拆 module - 包名统一用小写语义名:
api、impl、client,不要拼接前缀;导入时用别名仅用于消除同名冲突(如两个服务都有api包),不是默认操作 - 跨服务共享类型(如
User)必须定义在独立的shared/modelmodule 中,且该 module 不能 import 任何业务逻辑包
UserRepository.GetUserByID 返回值加了个新字段,所有实现和 mock 都得同步改,但没人通知测试组。解耦的前提是契约受控,而不是文件放对位置。


















