2025年8月,Google Wire 被官方归档。这个曾经号称"编译期依赖注入标准"的项目,凉了。
群里一个朋友当时就炸了:"我刚用 Wire 写完整个项目,你告诉我归档了?"
冷静。Wire 能用,但别指望新功能了。关键是,它本来也不太好用。
Wire 让人崩溃的三个瞬间
第一次崩溃:return nil, nil
go
func InitApp() (*App, error) {
wire.Build(NewDB, NewService, NewHandler)
return nil, nil // 这行永远不会执行
}
你第一次看到这段代码的时候,是不是也懵了?声明返回 *App,结果 return nil。为什么?因为 wire.Build 是编译期标记,运行时根本不走。但 Go 语法要求必须有返回值,所以你就得写这么一行废话。
这不是 Bug,这是 Feature。反人类的 Feature。
第二次崩溃:没有启动钩子
Wire 只负责一件事:把对象给你构造好。然后呢?启动服务、连接数据库、注册路由......这些你得自己写。
于是你的 main.go 变成了这样:
go
func main() {
app, _ := InitApp()
app.DB.Connect()
app.Server.Start()
app.Router.Register()
app.Cache.Warmup()
app.Metrics.Export()
// 漏一个,线上直接挂
}
每新增一个组件,你就得手动加一行。漏了?没人提醒你,直接上线,直接炸。
第三次崩溃:泛型不支持
Go 1.18 都发布三年了,Wire 对泛型的支持还是"手动包装"。
go
// 你想写这样?
dig.Provide(NewStore[int]) // 原生泛型,直接写
// Wire 必须这样:
func provideStoreInt() *Store[int] { return NewStore[int]() }
var Set = wire.NewSet(provideStoreInt)
每个类型写一个包装函数。代码膨胀得像国庆节的景区。
Fx 就完美了?想多了
Fx 的 API 确实漂亮,一行一个依赖,赏心悦目。但启动那一刻,你永远不知道会发生什么:
vbnet
panic: missing dependency: *DB (did you forget to provide it?)
编译通过了,测试跑完了,一启动就 panic。
为什么?Fx 依赖运行时反射。编译期不检查依赖,所有错误都在启动时等你。
项目越大,启动越慢。因为每次都要反射解析整个依赖树。本地改一行代码,重启等半天,开发效率直接腰斩。
接口返回 + 具体注入 = 定时炸弹
go
fx.Provide(func() interface{} { return &DB{} })
fx.Invoke(func(db *DB) { ... })
// 启动 panic:missing dependency
类型对不上?编译期看不出来,启动直接挂。
这种"编译通过,启动 panic"的体验,用过的都懂。
那用啥?dig 了解一下
dig 是一个编译期 DI 框架,写法像 Fx,安全像 Wire,但没 Wire 那些毛病。
go
//go:build digen
func InitApp() func(context.Context) error {
return dig.Build(
dig.Provide(NewConfig), // 普通构造函数
dig.Provide(NewDB),
dig.Supply(DefaultTimeout), // 直接注入值,不用包装
dig.Provide(func(t Timeout) *Server { return NewServer(t) }),
dig.Invoke(func(srv *Server) error { return srv.Run() }),
)
}
注意:没有 return nil, nil。
没有反射,全是编译期代码生成。
dig 是怎么解决这些痛点的?
1. 没有哑占位符
dig.Build 直接返回一个可执行的函数:
go
func InitApp() func(context.Context) error {
return dig.Build(...) // 返回函数,不是哑占位
}
符合直觉,新手也能看懂。
2. 原生泛型支持
go
dig.Provide(NewStore[int]) // 直接传泛型类型
dig.Provide(NewStore[string])
不用写包装函数,代码干净。
3. 内置 Invoke,不用手动维护清单
go
dig.Invoke(func(db *DB) error { return db.Ping() })
dig.Invoke(func(srv *Server) error { return srv.Start() })
依赖就绪后自动执行,新增组件加一行 Invoke,永远不会忘。
4. 闭包捕获检查
在 Provide 里写了内联闭包,不小心捕获了局部变量?go generate 直接报错:
bash
❌ cannot capture local variable "t"
Wire 对此静默通过,然后生成编译失败的代码,你根本看不出问题在哪。
5. 编译期检查
bash
go generate ./...
# 依赖缺失?循环引用?类型冲突?这里直接报错,不用等到运行时
编译期 vs 运行时:本质差异
| 维度 | dig / Wire | Fx |
|---|---|---|
| 依赖解析时机 | go generate 阶段 |
程序启动时(反射) |
| 依赖错误发现 | 生成代码时就报错,编译都过不了 | 运行时 panic,启动即挂 |
| 运行时开销 | 纯 Go 函数调用,无额外开销 | 反射解析类型、构建容器,有额外 CPU + 内存开销 |
| 二进制体积 | 纯生成的 Go 代码,无额外元数据 | 携带类型信息和反射元数据,体积更大 |
dig 和 Wire 是编译期方案:
go generate阶段完成依赖解析,错误在生成代码时就被拦截- 运行时只有纯 Go 函数调用,没有反射查找或类型解析
- 启动速度 = 手写
main.go的速度,没有任何框架损耗
Fx 是运行时方案:
- 程序启动时才用反射解析 Provider 签名、构建依赖图
- 每次启动都有解析开销,依赖越多越明显
- 运行时反射意味着:依赖漏了、类型冲突、循环引用......全部在启动那一刻才暴露
技术对比
说明:✅ 表示支持该特性,❌ 表示不支持,⚠️ 表示支持但有限制或代价,N/A 表示不适用。
| 维度 | dig | Wire | Fx |
|---|---|---|---|
| 定位 | 编译期生成 | 编译期生成 | 运行时反射 |
wire.Build 哑占位 |
❌ 不需要 | ❌ 必须写 return nil |
❌ 不需要 |
| 泛型 Provider | ✅ 原生支持 | ❌ 不支持(需手动包装) | ⚠️ 反射支持,有开销 |
| 启动钩子(Invoke) | ✅ 内置 | ❌ 无(手动维护) | ✅ 内置 |
| 闭包捕获检查 | ✅ 强制检查 | ❌ 无检查 | N/A |
| 模块定义方式 | 函数,可传参 | 包级变量,不灵活 | 函数,可传参 |
| 官方维护状态 | ✅ 活跃维护 | ❌ 已归档(2025.8) | ✅ 活跃维护 |
迁移成本高吗?
不高。
- API 和 Fx 基本一致,用 Fx 的直接无缝切换。
- 生成的代码是纯 Go,不依赖 dig 运行时。
- 5 分钟上手:
bash
go get github.com/shanjunmei/dig@v1.0.8
go install github.com/shanjunmei/dig/cmd/digen@latest
写在最后
Google 把 Wire 归档了,Fx 还在靠反射硬撑。
如果你:
- 受够了
return nil, nil - 不想维护手动初始化清单
- 不想看 Fx 的运行时 panic
- 想要编译期安全 + Fx 的 API 体验
dig 值得你花 5 分钟试试。