mcpField
G
golang-dependency-injection

golang-dependency-injection

github.com/samber开发3.7k39.9k 次安装5 天前更新
代码仓库

关于 golang-dependency-injection

面向 Go 的依赖注入技能,支持设计或重构 DI。它帮助团队选择手动组装或 DI 库,并规划易测试、低耦合的服务依赖图。

主要功能

  1. 01设计与重构模式
  2. 02手动注入和 DI 库比较
  3. 03构造函数注入指南
  4. 04使用子 Agent 分析重构
  5. 05通过注入模拟对象进行测试

适用场景

  1. 01组装新 Go 服务的依赖
  2. 02重构全局状态或紧耦合代码
  3. 03按项目规模选择 DI 方案
  4. 04使用模拟依赖测试服务

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/samber/cc-skills-golang --skill golang-dependency-injection

安装后在 Claude Code 中输入/golang-dependency-injection确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

evals/
evals.json
references/
google-wire.md
manual-di.md
samber-do.md
uber-dig-fx.md

详细介绍

角色: 你是一名 Go 软件架构师。你引导团队构建可测试、松耦合的设计——选择能够解决问题的最简单 DI 方案,绝不进行过度设计。

编排模式: 在将大型耦合代码库重构为依赖注入时,启动 Refactor 模式中描述的三个子 Agent(全局变量/init() 发现、具体依赖映射、服务定位器检测),并整合为一份迁移计划。在 Claude Code 中,使用 ultracode 显式启用多 Agent 编排。

模式:

  • 设计模式(新项目、新服务,或向现有 DI 设置添加服务):评估现有依赖图和生命周期需求;根据决策表推荐手动注入或某个库;然后生成接线代码。
  • 重构模式(现有耦合代码):最多使用 3 个并行子 Agent——Agent 1 找出全局变量和 init() 服务设置,Agent 2 映射应改为接口的具体类型依赖,Agent 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. 在接口边界使用 Mock——DI 让这一点变得轻而易举
  9. 保持依赖图浅层——过深的依赖链表明设计存在问题
  10. 根据项目规模和团队选择合适的 DI 库——见下方决策表

为什么需要依赖注入?

不使用 DI 时的问题DI 的解决方式
函数自行创建依赖注入依赖——可自由替换实现
测试需要真实数据库和 API在测试中传入 Mock 实现
更改一个组件会影响其他组件通过接口实现松耦合——组件无需了解彼此的内部细节
服务在各处初始化由集中式容器管理生命周期(单例、工厂、惰性初始化)
所有服务都在启动时加载惰性加载——仅在首次请求服务时创建
全局状态和 init() 函数在启动时显式接线——行为可预测,便于调试

DI 尤其适用于有许多相互连接服务的应用——HTTP 服务器、微服务、带插件的 CLI 工具。对于只有 2-3 个函数的小型脚本,手动接线即可。不要过度设计。

手动构造函数注入(不使用库)

对于小型项目,可以通过构造函数传递依赖项。完整的应用示例请参阅 手动 DI 示例。

// ✓ Good — explicit dependencies, testable
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 — manual wiring
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")
}
// ✗ Bad — hardcoded dependencies, untestable
type UserService struct {
    db *sql.DB
}

func NewUserService() *UserService {
    db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL")) // hidden dependency
    return &UserService{db: db}
}

在以下情况下,手动 DI 会变得难以应付:

  • 有 15 个以上存在交叉依赖的服务
  • 需要生命周期管理(健康检查、优雅关闭)
  • 希望实现延迟初始化或使用作用域容器
  • 装配顺序变得脆弱且难以维护

DI 库对比

Go 中主要有三种 DI 库方案:

决策表
标准手动google/wireuber-go/dig + fxsamber/do
项目规模小型(少于 10 个服务)中大型大型任意规模
类型安全编译时编译时(代码生成)运行时(反射)编译时(泛型)
代码生成无必需(wire_gen.go)无无
反射无无有无
API 风格不适用Provider 集合 + 构建标签结构体标签 + 装饰器简单的泛型函数
延迟加载手动不适用(全部急切加载)内置(fx)内置
单例手动内置内置内置
临时实例/工厂手动手动内置内置
作用域/模块手动Provider 集合模块系统(fx)内置(分层)
健康检查手动手动手动内置接口
优雅关闭手动手动内置(fx)内置接口
容器克隆不适用不适用不适用内置
调试打印语句编译错误fx.Visualize()ExplainInjector()、Web 界面
Go 版本任意任意任意1.18+(泛型)
学习曲线无中等高低
快速对比:装配风格

同一个依赖关系图——Config -> Database -> UserStore -> UserService -> API——可以手动装配,也可以通过容器装配。两者的区别在于装配代码所表达的内容:一套需要你维护的有序调用序列,或一组由容器替你排序的 Provider。

// Manual — you own the order; adding a dependency means editing every call site downstream
cfg := NewConfig()
db := NewDatabase(cfg)
store := NewUserStore(db)
svc := NewUserService(store)
api := NewAPI(svc)
api.Run()
// No shutdown hooks, health checks, or lazy loading — add them yourself

// Container (samber/do) — order is derived from the constructor signatures
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() // shutdown and health checks come from the container

google/wire 和 uber-go/fx 以不同方式表达同一个依赖关系图:wire 会在构建时根据 wire.Build Provider 列表生成上述手动调用序列(清理由 Provider 返回的 func() 处理,没有生命周期钩子);fx 则通过 fx.Provide 注册 Provider,并在运行时通过反射解析依赖,同时提供 OnStart/OnStop 钩子。各自的完整装配示例:google/wire、uber-go/dig + fx、samber/do。

使用 DI 进行测试

DI 让测试变得简单——注入模拟对象,而不是实际实现:

// Define a mock
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
}

// Test with manual injection
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) {
    // Create a test injector with mock implementation
    testInjector := do.New()

    // Provide the mock UserStore interface
    do.OverrideValue[UserStore](testInjector, &MockUserStore{
        users: map[string]*User{"1": {ID: "1", Name: "Alice"}},
    })

    // Provide other real services as needed
    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")
    // ... assertions
}

这在集成测试中特别有用:你希望大多数服务都使用真实实现,但需要模拟某个特定边界(数据库、外部 API、邮件发送器)。

何时采用 DI 库

信号建议
少于 10 个服务,依赖关系简单继续使用手动构造函数注入
10–20 个服务,存在一些横切关注点考虑使用 DI 库
20 个以上服务,需要生命周期管理强烈建议采用
需要健康检查、优雅关闭使用内置生命周期支持的库
团队不熟悉 DI 概念从手动方式开始,逐步迁移

常见错误

错误修正方法
使用全局变量作为依赖项通过构造函数或 DI 容器传递
使用 init() 设置服务在 main() 或容器中显式初始化
依赖具体类型在使用边界接受接口
到处传递容器(服务定位器)注入具体依赖项,而不是容器
依赖链过深(A->B->C->D->E)简化依赖链——大多数服务应直接依赖存储库和配置
每个请求都创建一个新容器每个应用使用一个容器;使用作用域隔离请求级依赖

交叉引用

  • → 有关 samber/do 使用模式的详细信息,请参阅 samber/cc-skills-golang@golang-samber-do 技能
  • → 有关接口设计和组合的信息,请参阅 samber/cc-skills-golang@golang-structs-interfaces 技能
  • → 有关使用依赖注入进行测试的信息,请参阅 samber/cc-skills-golang@golang-testing 技能
  • → 有关 DI 初始化位置的信息,请参阅 samber/cc-skills-golang@golang-project-layout 技能

参考资料

评论

支持 Markdown