使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射的容器、Provide/Invoke、dig.In/dig.Out 参数和结果对象、命名值、值组、可选依赖、作用域和 Decorate。适用于使用或采用 uber-go/dig 时,代码库导入 `go.uber.org/dig` 时,或在启动时组装应用程序图时。对于更高级的生命周期和模块,请参见 `samber/cc-skills-golang@golang-uber-fx` 技能。
角色: 你是一位使用 dig 组装应用程序图的 Go 架构师。你将容器保持在组合根,依赖接口而非具体类型,并将构造函数错误视为一等失败。
在 Go 中使用 uber-go/dig 进行依赖注入
基于反射的 DI 工具包,旨在为应用程序框架提供支持(它是 uber-go/fx 的引擎),并在启动时解析对象图。
官方资源:
本技能并非详尽无遗。请参考库文档和代码示例以获取更多信息。Context7 可作为可发现性平台提供帮助。有关 Go 包文档、版本、符号和已知漏洞,→ 请参见 samber/cc-skills-golang@golang-pkg-go-dev 技能。
go get go.uber.org/dig
dig 与 fx 对比
fx 构建在 dig 之上,并共享相同的容器引擎——DI 原语(Provide、Invoke、In/Out 结构体、命名值、值组)是相同的。fx.In/fx.Out 是 dig.In/dig.Out 的重新导出。
fx 在 dig 基础上添加的功能:
| 关注点 | dig | fx |
|---|---|---|
| DI 容器 | ✅ dig.New() |
✅(内嵌) |
| 生命周期钩子 | ❌ | ✅ fx.Lifecycle OnStart/OnStop |
| 模块系统 | ❌ | ✅ fx.Module 带作用域装饰器 |
| 信号感知的运行循环 | ❌ | ✅ app.Run() 阻塞等待 SIGINT/SIGTERM |
| 结构化事件日志 | ❌ | ✅ fx.WithLogger / fxevent |
| 启动/关闭超时 | ❌ | ✅ fx.StartTimeout / fx.StopTimeout |
选择 dig 当你只需要接线图时:CLI 工具、向调用者暴露容器的库、测试框架,或将 DI 嵌入到管理自身生命周期的现有应用中。
选择 fx 用于长时间运行的服务(HTTP 服务器、工作进程、守护进程)——生命周期和信号处理在那里是不可妥协的。请参见 samber/cc-skills-golang@golang-uber-fx 技能。
容器
import "go.uber.org/dig"
c := dig.New()
有用的选项:dig.DeferAcyclicVerification()(更快的启动)、dig.RecoverFromPanics()(将 panic 转换为 dig.PanicError)、dig.DryRun(true)(验证而不执行)。
Provide 和 Invoke
// 注册构造函数——惰性,仅在需要其输出时运行
err := c.Provide(func(cfg *Config) (*sql.DB, error) {
return sql.Open("postgres", cfg.DSN)
})
// 通过将服务作为函数参数请求,从容器中拉取服务
err = c.Invoke(func(db *sql.DB) error {
return db.Ping()
})
构造函数是惰性且记忆化的:每个输出类型只构建一次并共享(每个容器单例)。Provide 在注册时如果构造函数格式错误则报错;Invoke 返回构造函数的错误,并附带触发它的依赖路径。
dig 构造函数可以是任何函数。输入是依赖项,输出是提供的类型。error(最后一个返回值)表示构造失败。遵循“接受接口,返回结构体”。
使用 dig.In 的参数对象
一旦构造函数有 4 个以上依赖项,嵌入 dig.In 将它们分组为结构体字段并标记字段:
type HandlerParams struct {
dig.In
Logger *zap.Logger
DB *sql.DB
Cache *redis.Client `optional:"true"` // 如果未提供则为零值
DBRO *sql.DB `name:"readonly"` // 命名依赖
Routes []http.Handler `group:"routes"` // 值组
}
func NewHandler(p HandlerParams) *Handler { /* ... */ }
标签:name:"..."、optional:"true"、group:"..."。
使用 dig.Out 的结果对象
从一个构造函数返回多个值,并为结果附加 name/group 标签:
type ConnResult struct {
dig.Out
ReadWrite *sql.DB `name:"primary"`
ReadOnly *sql.DB `name:"readonly"`
}
func NewConnections(cfg *Config) (ConnResult, error) { /* ... */ }
命名值
同一类型的两个提供者会发生冲突。使用 dig.Name 消除歧义:
c.Provide(NewPrimaryDB, dig.Name("primary"))
c.Provide(NewReadOnlyDB, dig.Name("readonly"))
通过在 dig.In 字段中添加 name:"primary" / name:"readonly" 来消费。
值组
多个提供者,一个消费者切片——典型用于 HTTP 处理器、健康检查、迁移:
type RouteResult struct {
dig.Out
Handler http.Handler `group:"routes"`
}
func NewUserHandler(db *sql.DB) RouteResult { /* ... */ }
func NewPostHandler(db *sql.DB) RouteResult { /* ... */ }
type ServerParams struct {
dig.In
Routes []http.Handler `group:"routes"`
}
扁平化——附加 ,flatten(例如 group:"routes,flatten")以展开切片而不是嵌套它。组顺序不保证;如果顺序重要,请从单个构造函数提供显式的有序切片。
提供为接口(dig.As)
注册一个具体构造函数,并将其暴露为一个或多个接口,无需单独的适配器:
c.Provide(NewPostgresDB, dig.As(new(Database), new(io.Closer)))
// 消费者请求 Database 或 io.Closer;*PostgresDB 保持隐藏。
完整应用示例
func main() {
c := dig.New()
must(c.Provide(NewConfig))
must(c.Provide(NewLogger))
must(c.Provide(NewDatabase))
must(c.Provide(NewServer))
err := c.Invoke(func(srv *http.Server) error {
return srv.ListenAndServe()
})
if err != nil {
log.Fatal(err)
}
}
func must(err error) { if err != nil { panic(err) } }
dig 没有内置生命周期。如果你需要 OnStart/OnStop 钩子、信号处理和优雅关闭,请使用 fx——参见 samber/cc-skills-golang@golang-uber-fx 技能。
关于 Decorate、Scopes、可选依赖、错误辅助函数和 Visualize,请参见 advanced.md。
最佳实践
- 将容器保持在组合根——永远不要将
*dig.Container作为参数传递;将其视为main()的管道细节。服务定位器模式会破坏 DI 的可测试性优势。 - 依赖接口而非具体类型——允许在测试中交换实现而无需修改生产代码,并允许使用
dig.As从宽结构体暴露窄接口。 - 一旦构造函数有 4 个以上依赖项,优先使用参数对象(
dig.In结构体)——调用点保持可读性,添加新依赖只需一行更改,而不是破坏签名。 - 按模块分组注册(每个模块一个文件,调用
c.Provide为其类型)——审查和重构成为每个模块的关注点,并且以后可以提取模块为 fx.Module 而无需重写接线。 - 在测试中尽早验证图——在 CI 中对组合根调用
c.Invoke,以在启动时暴露缺失的提供者,而不是在第一次请求时。DryRun(true)跳过构造函数执行。 - 从构造函数返回错误而不是 panic——dig 用依赖路径包装它们,使失败点显而易见。
常见错误
| 错误 | 修复 |
|---|---|
| 将容器传递给服务 | 容器属于 main()。注入服务所需的类型化依赖;否则测试需要构建一个真实的容器。 |
同一类型的两个提供者没有 Name |
dig 在 Provide 时报错。要么命名它们,要么合并为一个返回 dig.Out 结果结构体的提供者。 |
忽略 Provide 错误 |
用 must 辅助函数包装每个 Provide。静默的注册错误会在更晚的时候变成缺失类型错误。 |
| 在顺序重要时使用组 | 组是无序的。如果顺序重要(中间件链、迁移序列),请使用一个构造函数提供显式的有序切片。 |
| 构造函数在导入时产生副作用 | 保持 init() 为空——只在构造函数内部开始工作,在图构建之后。 |
测试
dig 容器很廉价——每个测试构建一个新的,使用 Decorate 覆盖提供者,并调用 Invoke 来驱动系统。有关完整模式(每个测试的接线、共享辅助函数、CI 中的图验证、断言接线时错误、从构造函数 panic 中恢复),请参见 testing.md。
进一步阅读
- advanced.md — Decorate、Scopes、可选依赖、错误辅助函数、Visualize、完整快速参考
- recipes.md — 端到端示例:带路由组的 HTTP 服务器、两个数据库、请求作用域、装饰器、干运行验证
- testing.md — 测试模式和图验证
交叉引用
- → 参见
samber/cc-skills-golang@golang-uber-fx技能,了解基于 dig 构建的应用生命周期、模块和信号感知的 Run() - → 参见
samber/cc-skills-golang@golang-dependency-injection技能,了解 DI 概念和库比较 - → 参见
samber/cc-skills-golang@golang-samber-do技能,了解基于泛型且无反射的替代方案 - → 参见
samber/cc-skills-golang@golang-google-wire技能,了解编译时 DI(无运行时容器) - → 参见
samber/cc-skills-golang@golang-structs-interfaces技能,了解接口设计模式 - → 参见
samber/cc-skills-golang@golang-testing技能,了解通用测试模式
如果你遇到 uber-go/dig 中的错误或意外行为,请在 https://github.com/uber-go/dig/issues 提交 issue。






