Go依赖注入靠接口+构造函数参数+显式组装,NewXXX函数必须接收依赖,结构体字段用interface,构造函数校验nil,接口需包可见且方法最小化,fake实现比mock更轻量,main是唯一组装点。

Go 里没有“依赖注入框架”这回事,所谓“便于测试”,靠的是接口 + 构造函数参数传递 + 显式组装,三者缺一不可。用反射或代码生成器(比如 wire 或 dig)反而让测试更难定位问题。
为什么 NewXXX 函数必须显式接收依赖
构造函数是依赖注入的唯一入口。如果 NewUserService 不把 db DBClient 和 cache Cache 当参数,而是内部直接 new 出来或从全局变量取,那测试时就无法替换——mock 就无处安放。
- 结构体字段必须声明为 interface 类型,比如
db DBClient,不能是db *sql.DB - 构造函数要校验参数是否为 nil:
if db == nil { panic("db is required") },避免运行时nil pointer dereference - 不要接受模糊类型如
config map[string]interface{},它绕过编译检查,也让你没法在测试中精准控制行为
接口定义必须满足测试可实现性
测试能 mock 的前提是:接口在调用方包可见、方法集最小、签名稳定。否则 cannot use &fakeDB{} as DBClient 这类错误会反复出现。
- 接口名首字母必须大写(
DBClient),否则测试文件无法实现它 - 只提取实际用到的方法,比如只用
QueryRowContext和ExecContext,别照搬*sql.DB全部方法 - 避免把无关方法塞进一个接口,例如
UserRepository里混入日志或 HTTP 调用逻辑 - 返回值统一用
error,不暴露底层细节(如pq.Error)
测试时 fake 实现比 mock 更轻量可靠
多数场景下,手写一个 fakeDB 比引入 testify/mock 或 gomock 更快、更可控,且不会因 mock 配置错位导致误判。
立即学习“go语言免费学习笔记(深入)”;
- fake 结构体只需实现接口方法,返回预设值或错误:
return nil, errors.New("timeout") - 别在 fake 里做真实 IO(sleep、channel、网络调用),否则测试边界被污染
- 测试中直接传
&fakeDB{}给NewUserService,无需任何容器或注册步骤 - 如果需要验证调用参数,就在 fake 里加字段记录:
CalledWithSQL string,测试后断言
main 函数是唯一合法的依赖组装点
所有依赖必须在 main() 里自底向上创建并传递。这里出错,堆栈清晰;若分散在各包 init 或隐藏在 New 里,panic 时你根本不知道哪一层漏传了 *http.Client。
- 先初始化最底层依赖:logger、config、
*sql.DB、redis client - 再逐层 New 上层 service:
repo := NewUserRepo(db)→svc := NewUserService(repo, cache) - 最后把 svc 传给 HTTP handler 或 CLI 命令:
http.Handle("/user", &UserHandler{svc: svc}) - 绝不允许在 handler 内部 new service,否则测试时无法注入 fake
最容易被忽略的一点:interface 不是为测试而生,但它是 Go 唯一能让测试不碰生产逻辑的机制。一旦你开始用 any、map[string]interface{} 或反射绕过接口,测试就退化成对拼写和运气的测试。


















