golang-dependency-injection

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

2261Star
150Fork
更新于 2026/6/6
SKILL.md
只读
名称
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 设置添加服务):评估现有依赖图和生命周期需求;从决策表中推荐手动注入或某个库;然后生成编排代码。
  • 重构模式(现有紧耦合代码):使用最多 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 技能。

最佳实践总结

  1. 依赖必须通过构造函数注入——绝不要使用全局变量或 init() 进行服务设置
  2. 小型项目(< 10 个服务)应使用手动构造函数注入——无需库
  3. 接口必须在消费处定义,而非实现处——接受接口,返回结构体
  4. 绝不要使用全局注册表或包级服务定位器
  5. DI 容器必须仅存在于组合根(main() 或应用启动处)——绝不要将容器作为依赖传递
  6. 优先使用延迟初始化——仅在首次请求时创建服务
  7. 对有状态服务(数据库连接、缓存)使用单例,对无状态服务使用瞬态
  8. 在接口边界进行模拟——DI 使这变得简单
  9. 保持依赖图浅层——深层链表示设计问题
  10. 根据项目规模和团队选择合适的 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
项目规模 小型(< 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 初始化位置

参考