interface{}本身不是依赖注入起点,真正起点是自定义接口(如UserService),它定义能力契约;依赖应通过构造函数参数显式注入,避免隐藏或全局依赖,测试时优先用fake而非mock。

为什么 interface{} 是 Go 依赖注入的起点
Go 没有类、没有构造函数注入语法,依赖注入全靠“把具体实现换成接口变量”。不是为了抽象而抽象,而是为了让测试时能塞进假实现。interface{} 本身没用,但自定义接口(比如 UserService)才是关键锚点——它定义了“需要什么能力”,而不是“谁来提供”。
常见错误是过早抽象:给一个只被两个地方调用的结构体硬套接口,结果测试没变简单,维护成本先涨了。真正该抽接口的,是那些含外部依赖的组件:HTTP 客户端、数据库操作、定时器、文件读写。
- 只对有副作用或不确定性的依赖建接口(
DB.Query、http.Client.Do) - 避免为纯计算逻辑(如
CalculateTotal)建接口,它不阻碍测试 - 接口方法越少越好,按测试需要最小切片(比如测试只需 mock
Create,就别把Update和Delete塞进同一个接口)
如何用构造函数参数传递依赖(而不是全局变量)
把依赖作为参数传给结构体构造函数,是最直接、最易测的方式。不是所有“new”都叫构造函数——必须是显式接收依赖的函数,比如 NewOrderService 接收 repo OrderRepo 和 mailer Mailer。
容易踩的坑是悄悄藏依赖:在 func (s *Service) Do() error 里 new 一个 http.Client,或者从包级变量取 db。这样测试时根本没法替换,只能打桩(patch),而 Go 的打桩既难写又脆弱。
立即学习“go语言免费学习笔记(深入)”;
- 构造函数签名要暴露全部外部依赖,不隐藏、不延迟初始化
- 避免在结构体内嵌未导出字段(如
db *sql.DB)后,在方法里才初始化——这等于把耦合藏进了内部 - 如果依赖太多,考虑用选项模式(
Option函数),但别为了“优雅”牺牲可读性:8 个WithXxx()不如清清楚楚列 4 个参数
测试时怎么安全替换依赖:mock 还是 fake?
mock 是“行为驱动”的,适合验证调用次数、参数顺序;fake 是“状态驱动”的,适合模拟真实但轻量的实现(比如内存 map 替代 Redis)。Go 里多数场景 fake 更稳——mock 框架(如 gomock)生成代码冗长,且一旦接口改名,mock 就编译不过,还容易写出“过度断言”的测试。
典型错误是 mock 所有东西:连 time.Now() 都 mock,结果测试跑得比生产还慢,还掩盖了时间逻辑缺陷。其实只要把时间封装成接口(Clocker),测试时传个固定返回值的 fake 就行。
- 优先写 fake:比如用
map[string]string实现Cache接口,5 行搞定,零依赖 - 只对复杂交互 mock:比如第三方 API 返回特定错误码链路,需精确控制返回序列
- 别 mock 标准库函数(
os.ReadFile、time.Sleep)——它们都有标准接口可替换(fs.FS、Clocker)
依赖注入和测试解耦的边界在哪
不是所有耦合都要解。比如一个 HTTP handler 里直接调用 json.Marshal,不用抽接口——它无副作用、确定性强、标准库保障稳定。强行抽象只会让代码变厚、IDE 跳转变卡。
真正要警惕的是“测试时不得不启动数据库/网络/磁盘”的那一刻。那说明依赖没被拉到顶层,或者接口粒度太粗(比如一个 DataLayer 接口混着读写删,结果测试删数据时还得清库)。
- 解耦目标不是“100% 可 mock”,而是“能不启外部服务就跑通核心逻辑路径”
- 当发现写一个单元测试要 import 三个中间件包、还要 init 一个 config 结构体时,就是注入点设计过深了
- 最常被忽略的是“时间”和“随机数”——它们是隐式依赖,不显式注入,测试就不可重复










