
golang-dependency-injection
热门Go 语言依赖注入(DI)综合指南。涵盖 DI 的重要性(可测试性、松耦合、关注点分离、生命周期管理)、手动构造函数注入以及 DI 库对比(google/wire、uber-go/dig、uber-go/fx、samber/do)。在设计服务架构、设置依赖注入、重构紧耦合代码、管理单例或服务工厂时,或当用户询问 Go 中的控制反转、服务容器或依赖编排时,使用此技能。如需特定 DI 库,→ 请参阅 `samber/cc-skills-golang@golang-google-wire`、`samber/cc-skills-golang@golang-uber-dig`、`samber/cc-skills-golang@golang-uber-fx` 或 `samber/cc-skills-golang@golang-samber-do` 技能。
Go 语言依赖注入(DI)综合指南。涵盖 DI 的重要性(可测试性、松耦合、关注点分离、生命周期管理)、手动构造函数注入以及 DI 库对比(google/wire、uber-go/dig、uber-go/fx、samber/do)。在设计服务架构、设置依赖注入、重构紧耦合代码、管理单例或服务工厂时,或当用户询问 Go 中的控制反转、服务容器或依赖编排时,使用此技能。如需特定 DI 库,→ 请参阅 `samber/cc-skills-golang@golang-google-wire`、`samber/cc-skills-golang@golang-uber-dig`、`samber/cc-skills-golang@golang-uber-fx` 或 `samber/cc-skills-golang@golang-samber-do` 技能。
角色: 你是一名 Go 软件架构师。你引导团队走向可测试、松耦合的设计——你选择最简单的 DI 方法来解决问题,并且绝不过度设计。
模式:
- 设计模式(新项目、新服务,或向现有 DI 设置添加服务):评估现有依赖图和生命周期需求;从决策表中推荐手动注入或某个库;然后生成编排代码。
- 重构模式(现有紧耦合代码):使用最多 3 个并行子代理——代理 1 识别全局变量和
init()服务设置,代理 2 映射应变为接口的具体类型依赖,代理 3 定位服务定位器反模式(将容器作为参数传递)——然后汇总发现并提出迁移计划。
社区默认。 明确覆盖
samber/cc-skills-golang@golang-dependency-injection技能的公司技能具有优先权。
Go 中的依赖注入
依赖注入(DI)意味着将依赖传递给组件,而不是让组件自己创建或查找它们。在 Go 中,这是构建可测试、松耦合应用程序的方式——你的服务声明它们需要什么,然后调用者(或容器)提供它们。
此技能并非详尽无遗。使用 DI 库(google/wire、uber-go/dig、uber-go/fx、samber/do)时,请参考库的官方文档和代码示例以获取当前 API 签名。
有关基于接口的设计基础(接受接口,返回结构体),请参阅 samber/cc-skills-golang@golang-structs-interfaces 技能。
最佳实践总结
- 依赖必须通过构造函数注入——绝不要使用全局变量或
init()进行服务设置 - 小型项目(< 10 个服务)应使用手动构造函数注入——无需库
- 接口必须在消费处定义,而非实现处——接受接口,返回结构体
- 绝不要使用全局注册表或包级服务定位器
- DI 容器必须仅存在于组合根(
main()或应用启动处)——绝不要将容器作为依赖传递 - 优先使用延迟初始化——仅在首次请求时创建服务
- 对有状态服务(数据库连接、缓存)使用单例,对无状态服务使用瞬态
- 在接口边界进行模拟——DI 使这变得简单
- 保持依赖图浅层——深层链表示设计问题
- 根据项目规模和团队选择合适的 DI 库——请参阅下面的决策表
为什么需要依赖注入?
| 没有 DI 的问题 | DI 如何解决 |
|---|---|
| 函数创建自己的依赖 | 依赖被注入——自由切换实现 |
| 测试需要真实数据库、API | 在测试中传递模拟实现 |
| 更改一个组件会破坏其他组件 | 通过接口实现松耦合——组件不知道彼此的内部实现 |
| 服务随处初始化 | 集中式容器管理生命周期(单例、工厂、延迟) |
| 所有服务在启动时加载 | 延迟加载——仅在首次请求时创建服务 |
全局状态和 init() 函数 |
启动时显式编排——可预测、可调试 |
DI 在具有许多相互连接服务的应用程序中表现出色——HTTP 服务器、微服务、带插件的 CLI 工具。对于只有 2-3 个函数的小脚本,手动编排就足够了。不要过度设计。
手动构造函数注入(无库)
对于小型项目,通过构造函数传递依赖。请参阅 手动 DI 示例 获取完整应用程序示例。
// ✓ 好——显式依赖,可测试
type UserService struct {
db UserStore
mailer Mailer
logger *slog.Logger
}
func NewUserService(db UserStore, mailer Mailer, logger *slog.Logger) *UserService {
return &UserService{db: db, mailer: mailer, logger: logger}
}
// main.go — 手动编排
func main() {
logger := slog.Default()
db := postgres.NewUserStore(connStr)
mailer := smtp.NewMailer(smtpAddr)
userSvc := NewUserService(db, mailer, logger)
orderSvc := NewOrderService(db, logger)
api := NewAPI(userSvc, orderSvc, logger)
api.ListenAndServe(":8080")
}
// ✗ 坏——硬编码依赖,不可测试
type UserService struct {
db *sql.DB
}
func NewUserService() *UserService {
db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL")) // 隐藏依赖
return &UserService{db: db}
}
手动 DI 在以下情况下会失效:
- 你有 15 个以上具有交叉依赖的服务
- 你需要生命周期管理(健康检查、优雅关闭)
- 你想要延迟初始化或作用域容器
- 编排顺序变得脆弱且难以维护
DI 库对比
Go 有三种主要的 DI 库方法:
- google/wire 示例 — 编译时代码生成
- uber-go/dig + fx 示例 — 基于反射的框架
- samber/do 示例 — 基于泛型,无需代码生成
决策表
| 标准 | 手动 | google/wire | uber-go/dig + fx | samber/do |
|---|---|---|---|---|
| 项目规模 | 小型(< 10 个服务) | 中大型 | 大型 | 任意规模 |
| 类型安全 | 编译时 | 编译时(代码生成) | 运行时(反射) | 编译时(泛型) |
| 代码生成 | 无 | 需要(wire_gen.go) |
无 | 无 |
| 反射 | 无 | 无 | 是 | 无 |
| API 风格 | 不适用 | 提供者集合 + 构建标签 | 结构体标签 + 装饰器 | 简单、泛型函数 |
| 延迟加载 | 手动 | 不适用(全部急切) | 内置(fx) | 内置 |
| 单例 | 手动 | 内置 | 内置 | 内置 |
| 瞬态/工厂 | 手动 | 手动 | 内置 | 内置 |
| 作用域/模块 | 手动 | 提供者集合 | 模块系统(fx) | 内置(层次化) |
| 健康检查 | 手动 | 手动 | 手动 | 内置接口 |
| 优雅关闭 | 手动 | 手动 | 内置(fx) | 内置接口 |
| 容器克隆 | 不适用 | 不适用 | 不适用 | 内置 |
| 调试 | 打印语句 | 编译错误 | fx.Visualize() |
ExplainInjector()、Web 界面 |
| Go 版本 | 任意 | 任意 | 任意 | 1.18+(泛型) |
| 学习曲线 | 无 | 中等 | 高 | 低 |
快速对比:同一应用,四种方式
依赖图:Config -> Database -> UserStore -> UserService -> API
手动:
cfg := NewConfig()
db := NewDatabase(cfg)
store := NewUserStore(db)
svc := NewUserService(store)
api := NewAPI(svc)
api.Run()
// 无自动关闭、健康检查或延迟加载
google/wire:
// wire.go — 然后运行:wire ./...
func InitializeAPI() (*API, error) {
wire.Build(NewConfig, NewDatabase, NewUserStore, NewUserService, NewAPI)
return nil, nil
}
// 无生命周期钩子(OnStart/OnStop)或健康检查;通过提供者返回的 func() 进行清理
uber-go/fx:
app := fx.New(
fx.Provide(NewConfig, NewDatabase, NewUserStore, NewUserService),
fx.Invoke(func(api *API) { api.Run() }),
)
app.Run() // 管理生命周期,但基于反射
samber/do:
i := do.New()
do.Provide(i, NewConfig)
do.Provide(i, NewDatabase) // 自动关闭 + 健康检查
do.Provide(i, NewUserStore)
do.Provide(i, NewUserService)
api := do.MustInvoke[*API](i)
api.Run()
// defer i.Shutdown() — 自动处理所有清理
使用 DI 进行测试
DI 使测试变得简单——注入模拟实现而非真实实现:
// 定义模拟
type MockUserStore struct {
users map[string]*User
}
func (m *MockUserStore) FindByID(ctx context.Context, id string) (*User, error) {
u, ok := m.users[id]
if !ok {
return nil, ErrNotFound
}
return u, nil
}
// 使用手动注入进行测试
func TestUserService_GetUser(t *testing.T) {
mock := &MockUserStore{
users: map[string]*User{"1": {ID: "1", Name: "Alice"}},
}
svc := NewUserService(mock, nil, slog.Default())
user, err := svc.GetUser(context.Background(), "1")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if user.Name != "Alice" {
t.Errorf("got %q, want %q", user.Name, "Alice")
}
}
使用 samber/do 进行测试——克隆和覆盖
容器克隆会创建一个隔离的副本,你可以在其中仅覆盖需要模拟的服务:
func TestUserService_WithDo(t *testing.T) {
// 创建带有模拟实现的测试注入器
testInjector := do.New()
// 提供模拟的 UserStore 接口
do.OverrideValue[UserStore](testInjector, &MockUserStore{
users: map[string]*User{"1": {ID: "1", Name: "Alice"}},
})
// 根据需要提供其他真实服务
do.Provide[*slog.Logger](testInjector, func(i *do.Injector) (*slog.Logger, error) {
return slog.Default(), nil
})
svc := do.MustInvoke[*UserService](testInjector)
user, err := svc.GetUser(context.Background(), "1")
// ... 断言
}
这对于集成测试特别有用,你希望大多数服务是真实的,但需要模拟特定边界(数据库、外部 API、邮件发送器)。
何时采用 DI 库
| 信号 | 操作 |
|---|---|
| < 10 个服务,简单依赖 | 继续使用手动构造函数注入 |
| 10-20 个服务,存在一些横切关注点 | 考虑使用 DI 库 |
| 20 个以上服务,需要生命周期管理 | 强烈推荐 |
| 需要健康检查、优雅关闭 | 使用具有内置生命周期支持的库 |
| 团队不熟悉 DI 概念 | 从手动开始,逐步迁移 |
常见错误
| 错误 | 修复 |
|---|---|
| 全局变量作为依赖 | 通过构造函数或 DI 容器传递 |
使用 init() 进行服务设置 |
在 main() 或容器中显式初始化 |
| 依赖具体类型 | 在消费边界接受接口 |
| 到处传递容器(服务定位器) | 注入特定依赖,而非容器 |
| 深层依赖链(A->B->C->D->E) | 扁平化——大多数服务应直接依赖仓库和配置 |
| 每个请求创建新容器 | 每个应用一个容器;使用作用域实现请求级隔离 |
交叉引用
- → 请参阅
samber/cc-skills-golang@golang-samber-do技能以获取详细的 samber/do 使用模式 - → 请参阅
samber/cc-skills-golang@golang-structs-interfaces技能以获取接口设计和组合 - → 请参阅
samber/cc-skills-golang@golang-testing技能以获取使用依赖注入进行测试 - → 请参阅
samber/cc-skills-golang@golang-project-layout技能以获取 DI 初始化位置





