Go 的 interface 本质是为解耦设计,却天然适配测试:只要实现方法集即可替换依赖;定义需包可见、粒度小、聚焦协作边界;依赖注入靠参数传递,fake 实现只需返回预设值。

为什么 Go 的 interface 不是“为了测试而设计”,但偏偏最适合测试
Go 语言没有类继承、没有泛用的 mock 框架,却让单元测试异常轻量——关键在于:只要类型实现了 interface 的方法集,就能被任意替换。测试时你不需要动生产代码逻辑,只需构造一个满足接口签名的 fake 实现。
常见错误现象:cannot use &fakeDB{} as *fakeDB in assignment,本质是传参时用了具体类型指针,而非接口类型变量;或者函数签名硬编码了 *sql.DB,导致无法注入 mock。
- interface 定义必须在**调用方包可见**(首字母大写),否则测试文件无法实现它
- 不要为单个函数定义 interface;只为有明确边界、可能变化的协作对象抽象(如数据库访问、HTTP 客户端、消息队列)
- 接口越小越好,
Reader/Writer级别的粒度最稳妥;避免UserServiceInterface这种大而全的命名
如何把 *sql.DB 替换成可测试的 interface
直接依赖 *sql.DB 是测试最大障碍。正确做法是抽象出数据访问行为,而非结构体本身。
使用场景:任何需要查库、写库的业务逻辑,比如用户注册、订单创建。
立即学习“go语言免费学习笔记(深入)”;
参数差异:生产代码接收 DBClient 接口,测试代码传入 *fakeDB;两者方法签名一致,但实现完全不同。
性能影响:零。interface 是编译期静态检查 + 运行时一次间接跳转,开销可忽略。
- 定义接口时只保留实际用到的方法:
QueryRowContext、ExecContext,别照搬sql.DB全部 30+ 方法 - fake 实现不必模拟事务或连接池,只需返回预设值或错误:
return nil, errors.New("timeout") - 别在 fake 里做 sleep 或 channel 等真实 IO,那会污染测试边界
type DBClient interface {
QueryRowContext(context.Context, string, ...any) *sql.Row
ExecContext(context.Context, string, ...any) (sql.Result, error)
}
依赖注入不是靠框架,而是靠函数参数和构造函数
Go 没有运行时 DI 容器,所谓“注入”就是把依赖作为参数显式传进去。测试时你控制这个参数给什么,生产时给真实实现。
常见错误现象:nil pointer dereference,因为结构体字段未初始化,或构造函数没校验依赖是否为 nil。
兼容性影响:无。所有注入都发生在编译期,不引入额外依赖或反射。
- 服务结构体字段应为 interface 类型,而非具体实现:
db DBClient,不是db *sql.DB - 提供带依赖参数的构造函数:
NewUserService(db DBClient, cache CacheClient),禁止暴露无参构造 - 测试中直接 new fake 并传入:
svc := NewUserService(&fakeDB{}, &fakeCache{})
测试中 fake 的生命周期和状态管理要克制
fake 不是 mini 实现,而是“够用就行”的桩。过度模拟状态(比如自己维护 map 存 mock 数据)会让测试变脆、难以理解。
容易踩的坑:在多个 test case 间共享 fake 实例的内部状态,导致测试顺序敏感或偶然失败。
- 每个 test case 应创建独立 fake 实例,避免复用
- 用字段记录调用次数或最后一次参数,比维护完整数据模型更安全:
calledWithSQL string - 如果需要验证调用顺序,用切片记录调用日志,而不是在 fake 内部做复杂状态机
复杂点往往不在 interface 定义,而在于你是否愿意为测试提前拆分职责——比如把“生成 SQL”和“执行 SQL”拆成两个 interface。这比写十个 fake 更重要,也更容易被忽略。










