Go中函数模拟的最佳实践是面向接口依赖注入:将行为抽象为小而专的接口,通过构造时或参数注入可替换实现,避免直接mock函数或全局单例,从而实现高内聚、低耦合、可测试的代码设计。

用接口抽象真实依赖
Go里没法直接“mock一个包”,必须先把外部模块的行为抽成接口。比如你调用了 database/sql 的 QueryRow,就别在测试里直接 new 个 *sql.DB,而是定义一个 Querier 接口,只包含你真正用到的方法:
type Querier interface {
QueryRow(query string, args ...interface{}) *sql.Row
}
然后把业务函数改成接收这个接口,而不是具体类型。这样测试时就能传入任意实现——包括你自己写的 stub。
- 接口越小越好,只声明被测逻辑实际调用的方法
- 避免把
sql.DB整个塞进参数,它带太多无关方法,违反接口隔离原则 - 不要为“将来可能用到”而提前加方法,等真要测了再补
手动实现Stub结构体代替第三方库
很多场景根本不需要完整 Mock 框架。写一个轻量 StubDB 结构体,实现对应接口,控制返回值和错误即可:
type StubDB struct {
rows map[string]*sql.Row
err error
}
func (s *StubDB) QueryRow(query string, args ...interface{}) *sql.Row {
if s.err != nil {
return &sql.Row{} // 实际中需用 sql.ErrNoRows 或其他方式暴露错误
}
// 根据 query 或 args 返回预设结果
return mockRow("test@example.com")
}
这种写法没有额外依赖,调试直观,适合中小型项目或快速验证逻辑。
立即学习“go语言免费学习笔记(深入)”;
- 别在
StubDB里做复杂状态管理,容易让测试用例互相干扰 - 如果多个测试需要不同行为,用字段控制(如
ReturnError bool),而不是共享全局变量 - 注意
sql.Row本身不能直接构造,需用sqlmock或自己包装一层可测试的返回结构
依赖注入要显式传递,禁止包级全局变量
测试失败最常见的原因是:业务代码在函数内部直接调用了 logrus.StandardLogger()、redis.NewClient() 这类全局单例。一旦这么写,你就失去了替换能力。
正确做法是把依赖作为参数传入,或者通过结构体字段注入:
type UserService struct {
repo UserRepository // 接口类型,非 *sql.DB 或 *redis.Client
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
测试时构造 UserService 就能塞进任意 UserRepository 实现。
- 所有对外部模块的调用,都应经过你定义的接口层,而不是直连原始包
- 如果已有代码用了全局 client,先把它提取为结构体字段,再逐步替换调用点
- 别用
init()函数初始化依赖,它无法在测试中重置
慎用gomock等生成式Mock框架
gomock 生成的代码冗长、难调试,且对简单场景属于过度设计。它真正适合的是:接口方法多、调用顺序敏感、需要严格校验参数匹配的场景(比如 gRPC 客户端封装)。
多数 HTTP client、DB 查询、缓存操作,用手工 stub 更快更可控。
- 如果你只是想让
GetUser返回固定 JSON,用http.HandlerFunc写个内嵌 server 更简单 - gomock 的
EXPECT()链式调用容易写出强耦合测试,一改接口签名就全红 - 生成的 mock 文件要随接口更新,否则编译失败,增加维护成本
真正难的不是写 mock,而是识别哪些依赖必须隔离——比如发邮件、调支付网关、读配置文件。这些不隔离,测试就永远跑不快、不可靠。而像 time.Now() 这种看似“外部”的调用,其实只需用 func() time.Time 类型抽象,比 mock 包还轻量。


















