
为什么不用第三方 DI 框架也能做干净的依赖注入
Go 语言没有构造函数注入、反射自动装配这类“开箱即用”的 DI 支持,强行套用 Spring 或 Angular 那套思路反而会让代码更难读、更难测。真正有效的做法是:把依赖当成普通参数传进去,靠结构体字段 + 构造函数显式声明依赖关系。
常见错误现象:nil pointer dereference 因为忘了初始化某个依赖字段;测试时想 mock DB 却发现它被硬编码在方法内部;重构时改一个服务要翻遍十几个文件找 newService() 调用点。
- 所有外部依赖(如
*sql.DB、http.Client、自定义的UserService)都通过构造函数参数传入,不从全局变量或单例里拿 - 构造函数名统一用
NewXxx,返回指针,参数顺序按依赖强度从强到弱(比如先*sql.DB,再log.Logger) - 避免在构造函数里做重操作(如连接数据库),只做必要赋值;连接动作放到
Init()或首次使用时懒加载
怎么写可测试的服务结构体
测试难,往往是因为结构体把逻辑和依赖混在一起。关键不是“能不能 mock”,而是“要不要 mock”。比如 time.Now() 这种非纯函数调用,必须抽成接口或函数字段才能控制行为。
使用场景:写一个订单服务,需要调用支付网关、发邮件、记录日志。单元测试时,你不想真发邮件,也不希望每次跑测试都连真实支付接口。
立即学习“go语言免费学习笔记(深入)”;
- 把非确定性行为封装成字段,类型为函数或接口,例如:
SendEmail func(to, subject, body string) error - 结构体字段尽量用接口而非具体类型,比如用
mailer.Sender而不是*smtp.Client - 构造函数接受这些可替换的依赖,测试时直接传入闭包或 mock 实现,例如:
SendEmail: func(...){ return nil } - 不要为了“DI”而加一堆空接口或泛型包装,Go 的接口本就轻量,够用就行
什么时候该用 Wire 而不是手写 New 函数
Wire 是 Google 官方推荐的编译期 DI 工具,但它不是银弹。小项目或模块少时,手写 NewXxx 更快更直观;Wire 真正起作用的地方是依赖链深、组合方式多、且需要保证生命周期一致性的场景。
性能 / 兼容性影响:Wire 在构建时生成 Go 代码,不引入运行时开销,但会增加一次代码生成步骤;它不支持循环依赖,遇到 circular dependency 错误得手动拆解。
- 当构造函数参数超过 4 个,且其中多个来自同一层(比如多个 Repository),考虑用 Wire 统一管理
- 当存在多种启动配置(dev/staging/prod),每种需注入不同实现(如内存缓存 vs Redis),Wire 的 provider 链能减少重复代码
- 别用 Wire 去注入基本类型(如
string、int)或常量,它们应该走配置或环境变量 - Wire 生成的代码要
go fmt和go vet,否则 CI 可能失败;建议把wire.go文件单独放,不跟业务逻辑混
容易被忽略的边界:依赖生命周期与关闭资源
依赖注入不只是“给进来”,还得管“什么时候关”。比如 *sql.DB、*grpc.ClientConn、文件句柄,如果只注入不释放,程序会长时间持有资源,导致连接耗尽或文件锁无法释放。
复杂点在于:谁负责调用 Close()?注入方还是被注入方?答案是——由最外层容器(通常是 main 包或应用启动器)统一管理。
- 每个需要关闭的依赖,其构造函数应返回一个
io.Closer或带Close() error方法的接口 - 主程序中维护一个关闭队列(比如
[]func() error),按依赖创建逆序执行Close() - 结构体自身不要实现
Close()并在内部关掉传进来的依赖——这违反了依赖方向,也破坏了复用性 - 测试时如果用了临时
*sql.DB(如sqlite内存模式),记得在teardown阶段关掉,否则可能影响后续测试










