构造函数必须接收 interface 类型参数,如 NewUserService(repo UserRepository),而非具体类型;需校验 nil、字段声明为接口、main 函数组装依赖、手写 fake 更可靠。

构造函数必须接收 interface 类型参数
Go 里没有“自动注入”这回事,NewUserService 函数如果接收 *sql.DB 或 redis.Client 这类具体类型,测试时就无法替换成 fake 实现。接口才是解耦的起点。
常见错误:定义了 UserRepository 接口,但 NewUserService 却写成 func NewUserService(db *sql.DB) *UserService —— 这样一来,测试时根本没法塞进 &fakeRepo{}。
- 接口必须导出(首字母大写),且方法签名稳定,否则测试包里
cannot use &fake{} as UserRepository - 接口方法要最小化:只暴露业务真正需要的,比如
GetUserByID(id int) (*User, error),别把BeginTx、Close塞进去 - 避免在接口里混入无关行为,例如把日志或 HTTP 调用逻辑塞进
UserRepository
结构体字段声明为 interface,且构造函数校验 nil
字段类型写成 repo UserRepository,不是 repo *UserRepoImpl;否则编译期就失去多态能力,运行时也容易 panic。
构造函数里不校验 nil 是高频翻车点:生产环境跑得好好的,测试一 mock 就 panic,堆栈还指向某行 repo.GetUser(),根本看不出是传空了。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
if repo == nil { panic("repo is required") }必须有,且放在赋值前 - 不要用
config map[string]interface{}这种模糊类型代替明确接口,它绕过类型检查,测试时你连该 mock 哪个字段都不知道 - 字段名用语义化名称,比如
notifier比svc更易读、更易测
测试时 hand-written fake 比 mock 库更稳
多数场景下,手写一个 fakeUserRepo 结构体比引入 gomock 或 testify/mock 更快、更可靠。mock 库配置错一行,测试就通过但逻辑没走对。
典型 fake 写法就是实现接口方法,返回预设值或错误。别在里面加 sleep、channel、真实 DB 查询——那已经不是单元测试,是集成测试了。
- 需要验证调用参数?在 fake 里加字段记录:
CalledWithID int,测试后断言assert.Equal(t, 123, f.CalledWithID) - 模拟错误路径?直接
return nil, errors.New("timeout"),不用启动 mock server - fake 不要跨包复用,每个测试包自己写一个,避免隐式依赖和状态污染
main 函数是唯一合法的依赖组装点
所有依赖链必须从 main() 自底向上显式创建:先 new logger、config、DB,再 new repo,再 new service,最后传给 handler。任何一层漏传,panic 位置清晰,修复路径明确。
如果把 NewUserService 放在 handler 的 GET /user 里调用,或者藏在某个包的 init() 中,测试时你就得 patch 全局变量、重写 init 顺序——这不是测试,是逆向工程。
- handler 结构体字段也应该是 interface,比如
svc UserService,而不是svc *UserService - HTTP handler 不该知道 DB 连接细节,它只关心 “调
svc.GetUserInfo()能拿到字符串” - Wire 等生成式工具只是帮你写重复代码,它不改变“组装必须发生在 main”的本质;一旦生成代码出错,你还是要回到
main.go查依赖图

















