
你数过自己项目 main() 里有几个 NewXxx() 吗?
5 个,你可能觉得还好。10 个,开始要翻回去看顺序。15 个,你有没有在改一个依赖后,忘了改下游,线上 nil panic 告警?
Go 圈流行一句话:手动构造函数注入就够了,DI 框架是 Java 那套东西。
这句话对,直到你的项目跨过某条线。
我跑了一组实验(实测)。5 个依赖时手动 DI 和 Wire 没区别。15 个依赖,组装代码行数差 7.5 倍。30 个依赖,15 倍。
问题不是"手动 DI 不好",是"什么时候开始不好"。这取决于三道门槛。
main() 里几个 NewXxx()?
我用一个模拟 Go 项目,按链式和树状两种依赖拓扑,分别用手动和 Wire 实现,统计 main()/wire.go 中的组装代码行数——这是维护成本的代理指标。
链式依赖(每个服务只依赖前一个)的结果:
| 依赖数 | 手动组装行数 | Wire 组装行数* | 差距 |
|---|---|---|---|
| 5 | 5 | 2 | 2.5x |
| 15 | 15 | 2 | 7.5x |
| 30 | 30 | 2 | 15x |
*不含 provider 参数换行
手动 DI 的组装代码随依赖数线性增长,每多一个依赖,main() 多一行。Wire 是 Google 出品的编译时 DI 代码生成工具。你把所有 NewXxx 构造函数(Wire 叫 provider)注册到 wire.Build() 里,Wire 自己算出拼装顺序,生成手写风格的 Go 代码。Wire 的组装代码几乎不增长,wire.Build() 的调用结构始终是固定的,provider 增多只是多几个参数。
树状依赖更夸张。15 个服务(3 层树),手动组装约 35 行(实测,具体取决于树的形状和写法),Wire 还是固定结构,18 倍差距。
组装行数还在其次,改代码才是噩梦。
假设你的依赖链有 20 个节点,现在要在第 10 个位置插入一个新服务。手动 DI:所有依赖它的组装代码都要更新,改动范围取决于依赖图结构。Wire:加一个 NewServiceX 到 wire.Build(),完事。
依赖数的增长趋势比当前数量更致命。5 个依赖你用记事本都能写对,15 个就要反复检查,30 个你开始不敢改 main()。链式依赖已经是最好管理的场景了,树状依赖下,15 个服务就要 35 行。
第一道门槛:当你开始数 main() 里有几个 NewXxx() 的时候,手动 DI 的维护成本已经不低了。
规模还不是最难的部分。

编译过了,运行时为什么还炸?
我构造了另一个实验(实测):故意在初始化代码里制造一个错误,忘记提供某个依赖,直接传 nil。
手动 DI 版本:
|
|
编译?通过。运行?panic。
更阴险的场景,顺序倒置但不 crash:
|
|
编译通过,运行不 crash,但 repo.db 是 nil。这个 bug 可能上线了才发现。
Wire 呢?我故意少写一个 provider,运行 wire generate 的输出是这样的:
|
|
编译时拦住了,不是运行时炸了。
Fx 呢?Fx 在启动时用反射扫描你的构造函数,自动拼装依赖图。在 fx.New() 阶段校验依赖图完整性,缺少 provider 会立即报错。比运行时早,但比 Wire 晚。
| 方案 | 错误暴露时机 | 检测能力 |
|---|---|---|
| 手动 DI | 运行时 / 永远不暴露 | ❌ |
| Wire | 编译/代码生成时 | ✅ |
| Fx/Dig | 启动时 | ⚠️ |
犯错本身不可怕,可怕的是犯错了你不知道。15 个依赖的初始化顺序有多少种排法?15! ≈ 1.3 万亿种。你不可能穷举验证每一种组合。
认知过载不是能力问题,是信息量超出了人类可靠处理的范围。
初始化顺序出错,手动 DI 扛不住。
前提是你还在一个人写代码。多人协作呢?

三个人改 main.go,谁让谁?
模拟一个场景。3 人团队,10 个依赖的 Go 微服务。
周一早会:
- 小张给支付模块加风控服务
- 小李给通知模块加消息队列服务
- 小王给配置中心加 Redis 连接
手动 DI 下,三个人都要改 main.go。
小张在 main() 里加一行 svcD := NewServiceD(repo, logger)。小李也加一行。小王改 Config 结构体。
谁先合进 master,后面的人就解冲突。
在我模拟的场景中,3 次合并手动 DI 出了 2 次 merge conflict(实测)。Wire:0 次。
原因很简单——手动 DI 的组装代码集中在 main.go,而 Wire 的 provider 分散在不同文件。小张改 provider_service.go,小李改 provider_handler.go,小王改 provider_infra.go,各改各的。
有人会说:手动 DI 也可以拆文件啊。可以,拆构造函数是基本的 Go 工程实践,谁都会做。但拆了函数你还是要手动维护调用顺序和参数列表,Wire 替你推导了依赖关系。除非你把组装逻辑也抽成函数,但那不就是手动实现了一个简化版 Wire 吗?
三个人同时改 main(),你说谁让谁?

规模也好、顺序也好、协作也好,三道门槛是叠加的,不是三选一。那你的项目该怎么做决策?
你的项目该不该上 Wire?
三道门槛,每道都有量化指标:
- 依赖数 > 15?→ Wire
- 初始化顺序需要写注释才能理清?→ Wire
- 超过 2 个人同时改 main()?→ Wire
过一道就推荐 Wire。过两道,强烈推荐。
那 Fx 呢?先看实测数据:
| 方案 | 编译时间 | 二进制大小 | 启动时间 |
|---|---|---|---|
| 手动 DI | 70ms | 2,436 KB | 140ms |
| Wire | 78ms | 2,182 KB | 145ms |
| Fx | 301ms | 8,783 KB | 169ms |
测试环境:Go 1.26.2, macOS ARM64, 冷启动取 5 次中位数。编译时间 = go build 耗时,不含 wire generate 步骤(wire generate 单次约 200ms,仅在依赖变更时执行)。
上面的性能数据也验证了这一点,Wire 的编译时间和二进制大小与手动 DI 几乎没有差距,因为 wire generate 产出的就是你会手写的那段代码。Fx 多了 30ms 启动时间、3.6 倍二进制体积,这是反射的代价。
什么时候选 Fx?当你需要运行时动态性:插件系统、配置驱动的依赖替换、模块热加载。或者你需要 Fx 的生命周期钩子(OnStart/OnStop)做优雅启停,用 fx.Module 做模块化架构,按环境切换实现。大多数 Go 项目不需要这些,但需要的时候,Wire 替不了。
决策树:

简单说:过一道门槛就上 Wire,需要模块化架构或运行时动态性选 Fx,否则手动 DI。
迁移也不难。核心步骤:
- 新建
provider.go,把 NewXxx 构造函数从 main.go 移过去 - 新建
wire.go,用 wire.Build() 注册所有 provider - main() 从 10 行组装代码变成 2 行:
app := InitializeApp()+app.Run() - 如果是多人协作迁移,团队成员各维护自己的
provider_xxx.go,不再碰 main.go
核心业务逻辑不用动。但需要确保构造函数签名满足 Wire 的约定,比如接口类型需要 wire.Bind,返回 error 的构造函数需要额外处理。Wire 生成的代码你可以看、可以调试,它就是普通 Go 代码。
Wire 也不是银弹。wire generate 是额外的构建步骤,生成代码的堆栈追踪不如手写直观,团队需要学习 Wire 的 DSL。但对于过了三道门槛的项目,这些代价远小于手动 DI 的维护成本。

结语
手动 DI 不是不行。5 个依赖,手动写挺好。清晰、显式。
但"够用"是有边界的。5 个依赖够用,15 个依赖开始吃力,30 个依赖你不敢动 main()。初始化顺序错了编译器不帮你,多人改同一个文件 merge conflict 是常态。
三道门槛,核心判断只有一个:你还在享受手动 DI 的简洁,还是已经在忍受它的局限?
享受的时候不需要框架。开始忍受的时候,Wire 等你。如果你已经过了门槛,上面的决策树和迁移路径可以直接用。
