interface{} 不能解决依赖隔离问题,因其仅推迟类型检查至运行时,导致下游结构变更引发上游 panic;真正的隔离需定义最小契约接口,按需导出、使用方定义、职责单一。

为什么 interface{} 不能解决依赖隔离问题
很多人误以为用空接口 interface{} 就能“解耦”,结果只是把类型检查推迟到运行时。一旦下游模块返回了不符合预期的结构(比如少一个字段、多一个指针层级),上游调用直接 panic,且编译器完全不报错。
真正的接口隔离,是定义**最小契约**——只暴露当前模块需要的方法,不多不少。
定义依赖接口要遵循“按需导出”原则
假设你写了一个订单服务,需要调用用户服务查邮箱。不要引入整个 UserService 结构体或它的包,而是就地定义:
type UserEmailGetter interface {
GetEmailByID(ctx context.Context, id int64) (string, error)
}
然后让具体实现(比如 *httpUserClient 或 *mockUserStore)去实现它。这样:
• 订单模块编译不依赖用户服务的任何内部结构
• 测试时可直接传入 &mockUserEmailGetter{}
• 后续切换为 Redis 实现时,只要满足该接口,上层代码零修改
• 接口命名带动词(Getter/Sender/Validator)比泛泛的 UserRepo 更易理解用途
避免在接口中暴露非业务方法
常见错误是把 Close()、Start()、Config() 这类生命周期或配置方法塞进业务接口。这会导致:
• 单元测试必须模拟关闭逻辑,徒增复杂度
• HTTP 客户端和内存 mock 都得实现 Close(),但后者根本不需要
• 接口职责模糊,违反单一职责
立即学习“go语言免费学习笔记(深入)”;
正确做法是分离关注点:
• 业务行为 → 独立接口(如 EmailSender)
• 资源管理 → 单独结构体或初始化函数(如 NewSMTPClient(...) 返回 *smtp.Client,由调用方负责 defer Close)
• 配置注入 → 通过构造函数参数传入,而非接口方法
接口应定义在使用方,而非实现方
这是最容易被忽略的一点。如果把 UserEmailGetter 定义在用户服务包里,订单模块就得 import 用户服务——又绕回去了。
应该把接口定义在订单模块自己的 internal/order/dep/ 下,或者更推荐:放在订单模块的 interface.go 文件顶部(与业务逻辑同包)。
实现方只需 import 订单模块(仅为了实现接口),而不是反过来。
这样依赖方向清晰:订单 → 接口定义;用户服务 → 订单模块(仅用于实现)。循环依赖从源头杜绝。
接口不是用来“共享”的,是用来“约束使用方式”的。越贴近调用现场定义,越不容易膨胀,也越难被滥用。










