关于 golang-dependency-injection
面向 Go 的依赖注入技能,支持设计或重构 DI。它帮助团队选择手动组装或 DI 库,并规划易测试、低耦合的服务依赖图。
主要功能
- 01设计与重构模式
- 02手动注入和 DI 库比较
- 03构造函数注入指南
- 04使用子 Agent 分析重构
- 05通过注入模拟对象进行测试
适用场景
- 01组装新 Go 服务的依赖
- 02重构全局状态或紧耦合代码
- 03按项目规模选择 DI 方案
- 04使用模拟依赖测试服务
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-dependency-injection安装后在 Claude Code 中输入/golang-dependency-injection确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/evals.jsonreferences/google-wire.mdmanual-di.mdsamber-do.mduber-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 技能。
最佳实践摘要
- 必须通过构造函数注入依赖——绝不要使用全局变量或
init()进行服务设置 - 小型项目(少于 10 个服务)应使用手动构造函数注入——无需使用库
- 接口必须在使用处定义,而非实现场所——接收接口,返回结构体
- 绝不要使用全局注册表或包级服务定位器
- DI 容器必须仅存在于组合根(
main()或应用启动处)——绝不要将容器作为依赖传递 - 优先采用惰性初始化——仅在首次请求服务时创建它
- 有状态服务使用单例(数据库连接、缓存),无状态服务使用瞬态实例
- 在接口边界使用 Mock——DI 让这一点变得轻而易举
- 保持依赖图浅层——过深的依赖链表明设计存在问题
- 根据项目规模和团队选择合适的 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/wire 示例 — 编译时生成代码
- uber-go/dig + fx 示例 — 基于反射的框架
- samber/do 示例 — 基于泛型,无需生成代码
决策表
| 标准 | 手动 | google/wire | uber-go/dig + fx | samber/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技能