函数不应直接调用全局或包级函数(如f1()、time.Now()、http.Get()),而应将非纯行为抽象为接口或函数字段(如Clock接口),由调用方显式传入依赖,确保测试可控制、可预测、边界清晰。

函数不该直接调用全局或包级函数
直接调用 f1()、time.Now()、http.Get() 这类函数,会让测试无法控制返回值,也无法触发错误路径。Go 没有运行时函数 patch 机制,硬 mock 会破坏类型安全,甚至引入 reflect 或 unsafe 操作。
- 把非纯行为(如时间、网络、IO)抽成接口方法或函数字段,例如:
type Clock interface { Now() time.Time } - 不要为单个函数单独定义接口;只在协作边界抽象,比如“获取用户”“发送通知”,而不是“调用某个 http 方法”
- 避免把依赖藏在工具函数里——
func sendEmail(...) error看似简单,但测试时无法替换,必须变成结构体字段:SendEmail func(...)或接口
依赖必须显式传入,而非隐式获取
写 db := sql.Open(...) 或 svc := NewUserService() 在函数内部,等于把依赖和逻辑锁死。测试时只能绕过逻辑、打桩、改全局变量,结果是测试脆弱、难维护、容易漏覆盖。
- 构造函数(如
NewOrderService)应接收所有外部依赖作为参数,顺序按强弱:先DBClient,再Mailer,最后Logger - 结构体字段类型必须是接口,不是具体实现:
db DBClient,而不是db *sql.DB - 禁止无参构造函数;测试时若看到
svc := &OrderService{},说明它大概率没法测——字段未初始化,运行就 panic
接口定义必须由使用方控制
在 user 包里定义一个大而全的 UserRepositoryInterface,然后让 order 包去实现它,是典型反模式。这会导致接口膨胀、实现冗余、测试污染。
- 谁消费,谁定义:订单服务只需要查用户,就定义
UserGetter;支付服务需要冻结账户,就另定义UserUpdater - 接口越小越好,最好只有一两个方法;
Reader、Writer就是范本 - 接口名要体现用途,不是实现,比如用
ConnPool而不是ConnPoolInterface;Go 社区默认 interface 后缀冗余
mock 实现别追求“像真的一样”
写一个 fake DBClient 却试图模拟事务、连接池、重试逻辑,反而让测试变重、变慢、变不可靠。mock 的唯一目的是控制输入输出,验证被测逻辑是否按契约响应。
立即学习“go语言免费学习笔记(深入)”;
- fake 结构体只需实现接口方法,返回预设值或错误:
func (*fakeDB) QueryRowContext(...) *sql.Row { return &sql.Row{} } - 状态管理要克制:比如用 map 存几条测试数据就够了,不需要模拟锁、超时、并发竞争
- 不要在测试文件里 import 生产依赖包来“复用真实类型”——这会让测试间接依赖生产代码,失去隔离性


















