关于 golang-uber-dig
面向 Go 开发者的技能指南,介绍如何用 uber-go/dig 在应用组合根构建依赖图。涵盖构造器、参数与结果对象及测试验证,并说明 dig 与 fx 的职责边界。
主要功能
- 01注册并解析依赖图
- 02使用 dig.In 与 dig.Out
- 03配置命名值和分组值
- 04将具体实现暴露为接口
- 05验证图并测试构造器
适用场景
- 01为 CLI 工具组装依赖
- 02在测试中验证依赖图
- 03为多个实现区分命名依赖
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-uber-dig安装后在 Claude Code 中输入/golang-uber-dig确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/advanced.mdrecipes.mdtesting.md详细介绍
**Persona:**你是一名使用 dig 构建应用程序依赖关系图的 Go 架构师。你将容器放在组合根中,依赖接口而非具体类型,并将构造函数错误视为一等故障。
使用 uber-go/dig 在 Go 中进行依赖注入
基于反射的 DI 工具包,旨在为应用框架提供支持(它是 uber-go/fx 背后的引擎),并在启动时解析对象图。
官方资源:
本技能并非详尽无遗——更多信息请参阅库文档和代码示例:
- 有关 Go 包文档、符号、版本、导入者和已知漏洞,→ 请参阅
samber/cc-skills-golang@golang-pkg-go-dev技能(godig),查询 Go 包信息时优先于 Context7。 - 要在自己的代码中了解此库的用法(定义、调用点、诊断),→ 请参阅
samber/cc-skills-golang@golang-gopls技能(gopls)。 - 对于 pkg.go.dev 上没有索引的文档,Context7 仍可作为备用。
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
// Register a constructor — lazy, only runs when its output is needed
err := c.Provide(func(cfg *Config) (*sql.DB, error) {
return sql.Open("postgres", cfg.DSN)
})
// Pull a service out of the container by asking for it as a function parameter
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"` // zero value if not provided
DBRO *sql.DB `name:"readonly"` // named dependency
Routes []http.Handler `group:"routes"` // value group
}
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)))
// Consumers ask for Database or io.Closer; *PostgresDB stays hidden.
完整应用示例
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 skill。
有关 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。静默的注册错误会在很久之后才表现为缺少类型错误。 |
| 需要顺序时使用 groups | groups 不保证顺序。如果顺序很重要(中间件链、迁移序列),请使用一个构造函数提供显式排序的切片。 |
| 在导入时运行带副作用的构造函数 | 保持 init() 为空 — 只在依赖图构建完成后,于构造函数中启动工作。 |
测试
dig 容器开销很低 — 每个测试都构建一个全新的容器,使用 Decorate 覆盖提供器,并调用 Invoke 驱动系统。有关完整模式(每个测试的依赖装配、共享辅助函数、在 CI 中验证依赖图、断言装配时的错误,以及从构造函数 panic 中恢复),请参见 testing.md。
延伸阅读
- advanced.md — Decorate、Scopes、可选依赖、错误辅助函数、Visualize、完整快速参考
- recipes.md — 端到端示例:带路由组的 HTTP 服务、两个数据库、请求作用域、装饰器、空运行验证
- testing.md — 测试模式和依赖图验证
交叉引用
- → 有关基于 dig 实现的应用生命周期、模块和支持信号的 Run(),请参见
samber/cc-skills-golang@golang-uber-fxskill - → 有关 DI 概念和库比较,请参见
samber/cc-skills-golang@golang-dependency-injectionskill - → 有关无需反射的泛型替代方案,请参见
samber/cc-skills-golang@golang-samber-doskill - → 有关编译时 DI(无运行时容器),请参见
samber/cc-skills-golang@golang-google-wireskill - → 有关接口设计模式,请参见
samber/cc-skills-golang@golang-structs-interfacesskill - → 有关通用测试模式,请参见
samber/cc-skills-golang@golang-testingskill
如果您在 uber-go/dig 中遇到 bug 或意外行为,请在 https://github.com/uber-go/dig/issues 提交 issue。