Go中必须通过接口抽象才能可靠mock函数,因语言不支持运行时函数打桩;应定义小而专注的接口、显式注入依赖、避免全局状态,以保障并发安全与测试可控性。

Go里不能直接mock函数,得靠接口抽象
Go语言本身不支持运行时函数打桩(比如Python的patch或JavaScript的jest.mock),所以试图用全局变量替换f1、或给func类型赋新值来“覆盖”原实现,会在并发场景下出问题,且破坏包级初始化顺序。真正可行的路径只有一条:把依赖行为声明为接口,让被测逻辑接收该接口实例。
常见错误现象包括:
- 测试中调用
f4()触发真实HTTP请求,CI跑不通或超时 - 用
var f1 func() error = mockF1在测试前重赋值,但多个测试并行时互相污染 - 把
http.Client硬编码在结构体方法里,导致无法控制响应状态码或延迟
正确做法是定义最小接口,例如:
type HTTPDoer interface {
Do(*http.Request) (*http.Response, error)
}
然后让业务逻辑接收这个接口,而不是直接调用http.DefaultClient.Do。
立即学习“go语言免费学习笔记(深入)”;
接口要小而专注,别堆一堆方法
一个接口如果包含5个以上方法,基本等于没抽象——它既难实现,更难模拟,测试时还得写一堆空实现。接口的核心价值是契约清晰、替换成本低。
使用场景决定接口粒度:
- 只用一次远程调用?定义单方法接口:
type TokenGenerator interface { Generate(int) (string, error) } - 需要读写数据库?拆成
Reader和Writer两个接口,而非一个DataStore - 时间敏感逻辑(如过期判断)?抽象
Clock接口:type Clock interface { Now() time.Time },测试时传入固定时间
参数差异直接影响可测性:接受*sql.DB的函数比接受Querier接口(仅含QueryRowContext等必要方法)更难测,因为前者绑定了具体实现细节。
依赖必须显式注入,拒绝隐式全局状态
把var db *sql.DB放在包顶层,或者在函数里调用getDBFromGlobalPool(),会让测试变成“启动整个服务”的集成测试。每个测试用例都可能因上一个测试改了db的状态而失败。
重构要点:
- 把全局变量转为结构体字段:
type Service struct { db Querier } - 构造函数接收依赖:
func NewService(db Querier) *Service { return &Service{db: db} } - 避免
init()做I/O初始化;所有外部依赖(配置、client、clock)都在main或test中构建后传入 - HTTP handler里别用
context.Background(),统一从http.Request.Context()取
性能影响很小——接口调用在现代Go里几乎无开销;但兼容性提升明显:生产用RealHTTPClient,测试用MockHTTPClient,压测用带限流的ThrottledClient,全都不动业务逻辑。
测试时别绕开接口,直接伪造返回值
有人图省事,在测试里写obj.f2 = func() error { return nil },这看似快,但实际掩盖了设计缺陷:函数签名暴露了实现细节,且无法验证是否真调用了f2(比如条件分支漏了调用)。
真正有效的测试方式:
- 为接口写内存实现(不用第三方库):
type mockDB struct{ calls int }+ 实现QueryRowContext - 在方法里计数或记录参数,断言是否被调用、调用几次、参数是否符合预期
- 对
error分支全覆盖:传入mockDB{err: sql.ErrNoRows},验证业务逻辑是否正确处理 - 避免用
gomock生成大量样板代码;小项目手写mock更轻量、更易读
复杂点在于:抽象层不是越多越好,而是刚好够用。一个函数只依赖一个接口,比依赖三个接口更易维护;但若把DoStepOne和DoStepTwo塞进同一个接口,又会降低复用性。平衡点往往出现在“一次调用对应一个接口”时。


















