Persona: You are a Go software architect. You guide teams toward testable, loosely coupled designs — you choose the simplest DI approach that solves the problem, and you never over-engineer.
Modes:
- - Design mode (new project, new service, or adding a service to an existing DI setup): assess the existing dependency graph and lifecycle needs; recommend manual injection or a library from the decision table; then generate the wiring code.
- Refactor mode (existing coupled code): use up to 3 parallel sub-agents — Agent 1 identifies global variables and
init() service setup, Agent 2 maps concrete type dependencies that should become interfaces, Agent 3 locates service-locator anti-patterns (container passed as argument) — then consolidate findings and propose a migration plan.
Community default. A company skill that explicitly supersedes samber/cc-skills-golang@golang-dependency-injection skill takes precedence.
Dependency Injection in Go
Dependency injection (DI) means passing dependencies to a component rather than having it create or find them. In Go, this is how you build testable, loosely coupled applications — your services declare what they need, and the caller (or container) provides it.
This skill is not exhaustive. When using a DI library (google/wire, uber-go/dig, uber-go/fx, samber/do), refer to the library's official documentation and code examples for current API signatures.
For interface-based design foundations (accept interfaces, return structs), see the samber/cc-skills-golang@golang-structs-interfaces skill.
Best Practices Summary
- 1. Dependencies MUST be injected via constructors — NEVER use global variables or
init() for service setup - Small projects (< 10 services) SHOULD use manual constructor injection — no library needed
- Interfaces MUST be defined where consumed, not where implemented — accept interfaces, return structs
- NEVER use global registries or package-level service locators
- The DI container MUST only exist at the composition root (
main() or app startup) — NEVER pass the container as a dependency - Prefer lazy initialization — only create services when first requested
- Use singletons for stateful services (DB connections, caches) and transients for stateless ones
- Mock at the interface boundary — DI makes this trivial
- Keep the dependency graph shallow — deep chains signal design problems
- Choose the right DI library for your project size and team — see the decision table below
Why Dependency Injection?
| Problem without DI | How DI solves it |
|---|
| Functions create their own dependencies | Dependencies are injected — swap implementations freely |
| Testing requires real databases, APIs |
Pass mock implementations in tests |
| Changing one component breaks others | Loose coupling via interfaces — components don't know each other's internals |
| Services initialized everywhere | Centralized container manages lifecycle (singleton, factory, lazy) |
| All services loaded at startup | Lazy loading — services created only when first requested |
| Global state and
init() functions | Explicit wiring at startup — predictable, debuggable |
DI shines in applications with many interconnected services — HTTP servers, microservices, CLI tools with plugins. For a small script with 2-3 functions, manual wiring is fine. Don't over-engineer.
Manual Constructor Injection (No Library)
For small projects, pass dependencies through constructors. See Manual DI examples for a complete application example.
CODEBLOCK0
CODEBLOCK1
Manual DI breaks down when:
- - You have 15+ services with cross-dependencies
- You need lifecycle management (health checks, graceful shutdown)
- You want lazy initialization or scoped containers
- Wiring order becomes fragile and hard to maintain
DI Library Comparison
Go has three main approaches to DI libraries:
Decision Table
| Criteria | Manual | google/wire | uber-go/dig + fx | samber/do |
|---|
| Project size | Small (< 10 services) | Medium-Large | Large | Any size |
| Type safety |
Compile-time | Compile-time (codegen) | Runtime (reflection) | Compile-time (generics) |
|
Code generation | None | Required (
wire_gen.go) | None | None |
|
Reflection | None | None | Yes | None |
|
API style | N/A | Provider sets + build tags | Struct tags + decorators | Simple, generic functions |
|
Lazy loading | Manual | N/A (all eager) | Built-in (fx) | Built-in |
|
Singletons | Manual | Built-in | Built-in | Built-in |
|
Transient/factory | Manual | Manual | Built-in | Built-in |
|
Scopes/modules | Manual | Provider sets | Module system (fx) | Built-in (hierarchical) |
|
Health checks | Manual | Manual | Manual | Built-in interface |
|
Graceful shutdown | Manual | Manual | Built-in (fx) | Built-in interface |
|
Container cloning | N/A | N/A | N/A | Built-in |
|
Debugging | Print statements | Compile errors |
fx.Visualize() |
ExplainInjector(), web interface |
|
Go version | Any | Any | Any | 1.18+ (generics) |
|
Learning curve | None | Medium | High | Low |
Quick Comparison: Same App, Four Ways
The dependency graph: INLINECODE9
Manual:
CODEBLOCK2
google/wire:
CODEBLOCK3
uber-go/fx:
CODEBLOCK4
samber/do:
CODEBLOCK5
Testing with DI
DI makes testing straightforward — inject mocks instead of real implementations:
CODEBLOCK6
Testing with samber/do — Clone and Override
Container cloning creates an isolated copy where you override only the services you need to mock:
CODEBLOCK7
This is particularly useful for integration tests where you want most services to be real but need to mock a specific boundary (database, external API, mailer).
When to Adopt a DI Library
| Signal | Action |
|---|
| < 10 services, simple dependencies | Stay with manual constructor injection |
| 10-20 services, some cross-cutting concerns |
Consider a DI library |
| 20+ services, lifecycle management needed | Strongly recommended |
| Need health checks, graceful shutdown | Use a library with built-in lifecycle support |
| Team unfamiliar with DI concepts | Start manual, migrate incrementally |
Common Mistakes
| Mistake | Fix |
|---|
| Global variables as dependencies | Pass through constructors or DI container |
| INLINECODE10 for service setup |
Explicit initialization in
main() or container |
| Depending on concrete types | Accept interfaces at consumption boundaries |
| Passing the container everywhere (service locator) | Inject specific dependencies, not the container |
| Deep dependency chains (A->B->C->D->E) | Flatten — most services should depend on repositories and config directly |
| Creating a new container per request | One container per application; use scopes for request-level isolation |
Cross-References
- - → See
samber/cc-skills-golang@golang-samber-do skill for detailed samber/do usage patterns - → See
samber/cc-skills-golang@golang-structs-interfaces skill for interface design and composition - → See
samber/cc-skills-golang@golang-testing skill for testing with dependency injection - → See
samber/cc-skills-golang@golang-project-layout skill for DI initialization placement
References
角色设定: 你是一位 Go 软件架构师。你引导团队走向可测试、松耦合的设计——你选择能解决问题的最简单的依赖注入方法,并且从不过度设计。
模式:
- - 设计模式(新项目、新服务,或向现有依赖注入设置中添加服务):评估现有依赖图和生命周期需求;根据决策表推荐手动注入或使用某个库;然后生成连接代码。
- 重构模式(现有耦合代码):使用最多 3 个并行子代理——代理 1 识别全局变量和 init() 服务设置,代理 2 映射应成为接口的具体类型依赖,代理 3 定位服务定位器反模式(作为参数传递的容器)——然后整合发现结果并提出迁移计划。
社区默认。 一个明确取代 samber/cc-skills-golang@golang-dependency-injection 技能的公司技能具有优先权。
Go 中的依赖注入
依赖注入意味着将依赖项传递给组件,而不是让组件自己创建或查找它们。在 Go 中,这是构建可测试、松耦合应用程序的方式——你的服务声明它们需要什么,然后调用者(或容器)提供它们。
此技能并非详尽无遗。当使用依赖注入库(google/wire、uber-go/dig、uber-go/fx、samber/do)时,请参考该库的官方文档和代码示例以获取最新的 API 签名。
关于基于接口的设计基础(接受接口,返回结构体),请参阅 samber/cc-skills-golang@golang-structs-interfaces 技能。
最佳实践总结
- 1. 依赖项必须通过构造函数注入——绝不要使用全局变量或 init() 进行服务设置
- 小型项目(少于 10 个服务)应该使用手动构造函数注入——不需要库
- 接口必须在消费处定义,而不是在实现处定义——接受接口,返回结构体
- 绝不要使用全局注册表或包级服务定位器
- 依赖注入容器必须只存在于组合根(main() 或应用启动处)——绝不要将容器作为依赖项传递
- 优先使用懒初始化——仅在首次请求时创建服务
- 对有状态的服务使用单例(数据库连接、缓存),对无状态的服务使用瞬态
- 在接口边界进行模拟——依赖注入使这变得非常简单
- 保持依赖图浅层——深层链表明设计存在问题
- 根据项目规模和团队选择合适的依赖注入库——请参阅下面的决策表
为什么要使用依赖注入?
| 没有依赖注入的问题 | 依赖注入如何解决 |
|---|
| 函数创建自己的依赖项 | 依赖项被注入——可以自由切换实现 |
| 测试需要真实的数据库、API |
在测试中传递模拟实现 |
| 更改一个组件会破坏其他组件 | 通过接口实现松耦合——组件不知道彼此的内部实现 |
| 服务随处初始化 | 集中式容器管理生命周期(单例、工厂、懒加载) |
| 所有服务在启动时加载 | 懒加载——仅在首次请求时创建服务 |
| 全局状态和 init() 函数 | 在启动时显式连接——可预测、可调试 |
依赖注入在具有许多相互连接服务的应用程序中表现出色——HTTP 服务器、微服务、带有插件的 CLI 工具。对于只有 2-3 个函数的小脚本,手动连接就足够了。不要过度设计。
手动构造函数注入(无库)
对于小型项目,通过构造函数传递依赖项。请参阅 手动 DI 示例 以获取完整的应用程序示例。
go
// ✓ 好——显式依赖,可测试
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)
}
go
// ✗ 坏——硬编码依赖,不可测试
type UserService struct {
db *sql.DB
}
func NewUserService() *UserService {
db, := sql.Open(postgres, os.Getenv(DATABASEURL)) // 隐藏依赖
return &UserService{db: db}
}
手动依赖注入在以下情况下会失效:
- - 你有 15 个以上具有交叉依赖的服务
- 你需要生命周期管理(健康检查、优雅关闭)
- 你想要懒初始化或作用域容器
- 连接顺序变得脆弱且难以维护
依赖注入库比较
Go 主要有三种依赖注入库的方法:
决策表
| 标准 | 手动 | 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
手动:
go
cfg := NewConfig()
db := NewDatabase(cfg)
store := NewUserStore(db)
svc := NewUserService(store)
api := NewAPI(svc)
api.Run()
// 没有自动关闭、健康检查或懒加载
google/wire:
go
// wire.go — 然后运行: wire ./...
func InitializeAPI() (*API, error) {
wire.Build(NewConfig, NewDatabase, NewUserStore, NewUserService, NewAPI)
return nil, nil
}
// 不支持关闭或健康检查
uber-go/fx:
go
app := fx.New(
fx.Provide(NewConfig, NewDatabase, NewUserStore, NewUserService),
fx.Invoke(func(api *API) { api.Run() }),
)
app.Run() // 管理生命周期,但基于反射
samber/do:
go
i := do.New()
do.Provide(i, NewConfig)
do.Provide(i, NewDatabase) // 自动关闭 + 健康检查
do.Provide(i, NewUserStore)
do.Provide(i, NewUserService)
api := do.MustInvoke*API
api.Run()
// defer i.Shutdown() — 自动处理所有清理工作
使用依赖注入进行测试
依赖注入使测试变得简单——注入模拟实现而不是真实实现:
go
// 定义一个模拟
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