Go单元测试依赖真实服务是因为未将依赖抽象为窄接口,应仅对可能替换的协作点(如I/O)建模,接口只描述行为、方法精简,依赖注入时传interface而非具体类型,避免过度抽象。

为什么你的 Go 单元测试总在调用真实 HTTP 客户端或数据库
因为没把依赖抽象成 interface,而是直接 new 了具体类型。Go 的接口是隐式实现的,你不需要显式声明“implements”,但必须提前设计好契约——否则测试时只能打桩整个包、改全局变量,或者干脆跳过集成测试。
关键不是“要不要 interface”,而是“这个 interface 是否只描述行为,且足够窄”。比如一个发短信的服务,别定义 SmsService 接口包含 Send、Retry、Log,而应该只留 Send(ctx context.Context, to, msg string) error。其他逻辑属于实现细节,不该暴露给调用方。
- 测试时,你可以用 struct 实现该接口,返回固定错误或延迟响应,无需启动真实服务
- 生产代码里注入真实实现(如基于
twilio.Client封装的结构体),和测试代码完全解耦 - 如果接口方法过多、参数太重(比如塞了 5 个字段的 struct),说明它已经承担了太多职责,容易导致 mock 失真或测试脆弱
如何写一个既可测试又不泛滥的 interface
Go 里不是每个类型都要配 interface。只对「可能被替换」或「已有多种实现」的协作点建模。典型场景:外部 I/O(HTTP、DB、文件)、第三方 SDK、需要模拟失败路径的业务逻辑。
看这个反例:type UserRepository struct{ db *sql.DB } 直接暴露 *sql.DB,测试就得连真实数据库;正解是定义 type UserRepo interface { GetByID(id int) (*User, error) },再让具体实现去处理 *sql.DB 的 query 细节。
立即学习“go语言免费学习笔记(深入)”;
- 接口名以
er结尾(Reader、Writer、Sender)更符合 Go 习惯,也暗示其行为契约 - 避免在接口里放字段或构造函数;接口只管“能做什么”,不管“怎么做成”
- 不要为单个实现提前抽象——等第二版实现出现(比如从内存 map 切到 Redis)再提取 interface 更稳妥
依赖注入时传 interface 还是 struct 指针
必须传 interface 类型参数,而不是具体 struct。否则调用方就又和实现绑死了。常见错误是函数签名写成 func NewService(u *UserRepository),这等于宣告“我只接受这个 struct”,彻底堵死 mock 路径。
正确做法是:func NewService(repo UserRepo) *Service,其中 UserRepo 是接口类型。这样测试时传个 &mockUserRepo{} 就行,生产环境传 &postgresUserRepo{db: ...}。
- struct 字段也建议存 interface(如
repo UserRepo),而非具体类型,否则初始化后无法替换 - 不要用
interface{}替代明确接口——它失去类型约束,编译器没法检查方法是否实现,测试时容易 panic - 如果依赖太多,考虑用配置 struct + interface 组合,但别为了“看起来整洁”把所有依赖塞进一个
Depends接口里
gomock 或 testify/mock 什么时候反而让测试更难维护
当你开始为 interface 写大量 EXPECT().Return(...).Times(3),说明接口设计过宽,或测试在验证实现细节而非行为。Go 原生推荐轻量 mock:手写一个匿名 struct 或小 struct 实现接口,几行代码搞定。
例如测试超时逻辑,你只需要:
mock := &fakeSender{failOnTimeout: true},而不是配置 mock 框架去拦截某个方法第 2 次调用才返回 error。
- gomock 生成的 mock 代码会随 interface 变动频繁更新,增加 PR 噪声
- testify/mock 在泛型普及后兼容性变差,尤其涉及
any或约束类型时容易编译失败 - 真正难测的从来不是 interface 本身,而是那些没被抽离的副作用——比如函数内直接调用
time.Now()、log.Printf、os.Getenv,这些得靠函数变量或配置项导出,不是 interface 能解决的










