
Go 单元测试里为什么非得用接口做 Mock
因为 Go 没有类继承、没有虚函数表,mock 不是靠“重写方法”实现的,而是靠“换掉整个依赖对象”。而能被干净替换的,只有接口类型——结构体、函数、具体类型都绑死在调用链里,没法在测试时无缝插拔。
常见错误现象:cannot use *MyDB (type *MyDB) as type DBInterface in argument to NewService,本质是你没把生产代码里的具体类型提前抽象成接口。
- 所有对外依赖(DB、HTTP client、缓存、消息队列)必须先定义接口,且接口只包含当前业务真正用到的方法(避免过度设计)
- 构造函数或初始化逻辑中,接收接口而非具体类型,例如
NewUserService(db UserDB)中的UserDB是接口 - 不要在测试里直接改结构体字段去“伪造行为”,那不是 Mock,是脆弱的打补丁
手动 Mock 接口比用 gomock 更快更可控
gomock 生成代码冗长、更新接口后要重跑命令、还容易带出 MockCtrl 生命周期管理问题。对中小项目,手写一个匿名结构体或小结构体,5 行内搞定,还一眼看懂逻辑。
使用场景:你只需要模拟 1–3 个方法返回固定值、或简单校验入参,比如 GetUser(id int) (*User, error) 返回 nil, errors.New("not found")。
立即学习“go语言免费学习笔记(深入)”;
- 直接在测试函数里写:
mockDB := &mockUserDB{wantErr: true},然后实现GetUser方法按需返回 - 避免给 mock 结构体加多余方法——只实现测试用到的,否则编译过不了(Go 接口实现是隐式的,但必须全满足)
- 如果需要记录调用次数或参数,加字段如
calledWithID int,在方法里赋值,测试末尾断言它
type mockUserDB struct {
wantErr bool
calledWithID int
}
func (m *mockUserDB) GetUser(id int) (*User, error) {
m.calledWithID = id
if m.wantErr {
return nil, errors.New("not found")
}
return &User{ID: id}, nil
}
接口定义太宽会导致 Mock 成本飙升
比如定义 type DB interface { Query(...); Exec(...); Begin(); Commit(); Rollback(); Ping() },结果测试里为了测一个 GetUser,还得实现全部 6 个方法——哪怕其他方法根本不会被调用。
性能影响倒不大,但可维护性崩了:每次改一个无关方法签名,所有 mock 都要同步改;更糟的是,它掩盖了真实依赖关系,让重构和阅读变困难。
- 按用法拆接口:一个 service 只依赖
UserReader和UserWriter,而不是大一统DB - 接口名体现角色,不体现实现,比如
PaymentClient比HTTPPaymentClient合理 - 允许同一类型实现多个小接口,Go 不限制——这才是接口组合的设计本意
测试里传 mock 对象却没生效?检查依赖注入路径
最常踩的坑:mock 对象造好了,也传进去了,但运行时还是走真实 DB。原因几乎都是——生产代码里 new 出了具体类型,绕过了你传入的接口。
典型错误代码:func (s *Service) DoWork() { db := &RealDB{}; db.Query(...) },这里 db 是硬编码的,测试传的 s.db 根本没被用。
- 所有外部依赖必须通过字段或参数注入,禁止在方法内部
new或&具体类型 - 构造函数是唯一允许 new 具体类型的入口,其余地方只用字段或参数持有的接口
- 用
go vet -shadow或 IDE 的未使用变量提示,能快速发现“声明了参数却没用”的低级错误










