golang-uber-dig

golang-uber-dig

热门

使用 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` 技能。

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
golang-uber-dig
描述

使用 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 原语(ProvideInvokeIn/Out 结构体、命名值、值组)是相同的。fx.In/fx.Outdig.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

最佳实践

  1. 将容器保持在组合根——永远不要将 *dig.Container 作为参数传递;将其视为 main() 的管道细节。服务定位器模式会破坏 DI 的可测试性优势。
  2. 依赖接口而非具体类型——允许在测试中交换实现而无需修改生产代码,并允许使用 dig.As 从宽结构体暴露窄接口。
  3. 一旦构造函数有 4 个以上依赖项,优先使用参数对象(dig.In 结构体)——调用点保持可读性,添加新依赖只需一行更改,而不是破坏签名。
  4. 按模块分组注册(每个模块一个文件,调用 c.Provide 为其类型)——审查和重构成为每个模块的关注点,并且以后可以提取模块为 fx.Module 而无需重写接线。
  5. 在测试中尽早验证图——在 CI 中对组合根调用 c.Invoke,以在启动时暴露缺失的提供者,而不是在第一次请求时。DryRun(true) 跳过构造函数执行。
  6. 从构造函数返回错误而不是 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。