AlphaGao's Blog

恐惧源于火力不足,紧张源于准备不足!

0%

Go 依赖注入以及 wire 最佳实践

在 Go 项目里,依赖关系的初始化通常是件麻烦事。最开始的时候,我们往往会在 main.go 里直接 new 一堆对象,把它们一个个串起来。比如先建 db,然后基于 db 创建 dao,再基于 dao 创建 service,最后才有上层的 handler 或 api。

这没什么问题,小项目完全够用。但随着项目逐渐变大,依赖越来越多,初始化函数就会越来越长。每次有人加一个新的 service,你就要手动改一堆代码,稍不注意就漏掉了。

于是很多人会问:Go 里有没有更好的依赖注入方式?有的兄弟,有的。

手写依赖注入的问题

我们先看一个很常见的例子:

1
2
3
4
5
6
db := NewDB()
userDao := NewUserDao(db)
orderDao := NewOrderDao(db)

userService := NewUserService(userDao)
orderService := NewOrderService(orderDao, userDao)

这种写法直观,优点是没有黑箱。所有依赖都能在一眼看懂的地方被串起来。
问题在于:当依赖很多的时候,你要手写的初始化逻辑也会很多,而且这些逻辑往往是重复的。

这时候就会有人想到自动化:有没有办法把“依赖如何组合”的规则声明出来,剩下的初始化代码自动生成?

Wire 的出现

Google 开源的 Wire 就是做这件事的。

它的定位很简单:在编译期自动生成依赖注入代码。
这意味着它跟 Java 或 Spring 那种运行时容器不一样。Wire 并不在程序运行的时候动态查找依赖,而是在你 go build 的时候,帮你生成一份 wire_gen.go,里面就是完整的初始化逻辑。

换句话说:用 Wire 的结果,和你手写初始化代码是一样的。只是 Wire 替你写好了。

Wire 的基本用法

用法其实挺简单的:

1、你要有一堆构造函数,比如:

1
2
3
func NewUserDao(db *gorm.DB) *UserDao { ... }
func NewOrderDao(db *gorm.DB) *OrderDao { ... }
func NewUserService(dao *UserDao) *UserService { ... }

这些构造函数本来就要写的,无论是否使用 wire 做依赖注入;

2、把它们放进一个 ProviderSet 里:

1
2
3
4
5
var ProviderSet = wire.NewSet(
NewUserDao,
NewOrderDao,
NewUserService,
)

3、写一个注入函数作为 wire 的入口:

1
2
3
4
func InitUserService(db *gorm.DB) *UserService {
wire.Build(ProviderSet)
return nil
}

然后运行 wire 命令,Wire 就会生成一个 wire_gen.go 文件,里面帮你把 db → dao → service 全部串起来。

Wire 的原理

理解 Wire 原理很重要,否则容易觉得它“有点魔法”。其实它做的事很直接:

1、解析 wire.Build 里提到的 ProviderSet

2、分析每个构造函数的输入和输出类型;

3、自动生成一份依赖拼装代码;

4、编译时用生成的代码,不引入任何运行时开销。

所以 Wire 的思路完全是静态的,所有问题都会在编译阶段暴露。比如有依赖没声明,就会直接编译失败。

使用 Wire 的优缺点

这里分享一些我在实际项目里遇到的感受。

优点:

  • 省去大量样板代码,少写一堆 NewXxx 串联逻辑。
  • 类型安全,缺少依赖时编译器直接报错。
  • ProviderSet 可以分模块维护,解耦明显。
  • 没有运行时损耗,比动态容器更轻。

缺点:

  • 每次新增依赖都要动 ProviderSet,并跑一遍 wire
  • 生成的 wire_gen.go 有时很长,不适合人工阅读。
  • 团队成员需要理解 Wire 的原理,否则看不到 wire_gen.go 会一头雾水。

个人感觉的最佳实践

经过一些踩坑,我觉得有几个点比较重要:

1、wire_gen.go 不要提交 PR

  • 文件太大,diff 没意义,还容易冲突。
  • 在 CI/CD 里执行 wire ./...,保证始终可复现。
  • 可以加检查脚本,确保开发和 CI 生成的文件一致。

2、ProviderSet 按模块拆分

  • dao 层一个 set,service 层一个 set,最后再汇总。
  • 这样可读性强,也便于复用。

3、构造函数统一风格

  • 命名用 NewXxx,避免歧义。
  • 参数显式传入,依赖不要藏在函数里。

4、适合的使用场景

  • 小项目:完全没必要,用手写更清晰。
  • 中大型项目:依赖多,service 层级深,Wire 才能发挥价值。
  • 微服务:每个服务独立一份 ProviderSet,初始化成本低。

总结

Wire 本质上就是一个“帮你写初始化代码的工具”。它不神秘,也没有运行时开销,适合用在依赖复杂的中大型 Go 项目里。

但要注意:工具不会自动降低复杂度。如果团队里缺少规范,Wire 可能会让工程更难懂。反过来,如果大家统一好 ProviderSet 的组织方式,并把 wire_gen.go 交给 CI 管理,它就能成为一个减轻负担的好帮手。

所以我的建议是:小项目手写即可

中大型项目用 Wire,但要配合 CI/CD、模块化 ProviderSet 一起使用。