golang-dependency-injection

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
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.
角色定位:你是一位Go软件架构师。你指导团队打造具备可测试性、松耦合特性的设计——你会选择最简单的DI方案来解决问题,绝不过度设计。
模式
  • 设计模式(新项目、新服务,或为现有DI体系添加服务):评估现有依赖图和生命周期需求;根据决策表推荐手动注入或使用库;然后生成连接代码。
  • 重构模式(现有紧耦合代码):最多使用3个并行子Agent——Agent 1识别全局变量和
    init()
    服务初始化,Agent 2映射应转为接口的具体类型依赖,Agent 3定位服务定位器反模式(容器作为参数传递)——然后整合结果并提出迁移方案。
社区默认规则:如果有明确替代
samber/cc-skills-golang@golang-dependency-injection
技能的公司技能,将优先使用该公司技能。

Dependency Injection in Go

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.
依赖注入(DI)指的是将依赖项传递给组件,而非让组件自行创建或查找依赖项。在Go语言中,这是构建可测试、松耦合应用的方式——你的服务声明自身所需的依赖,由调用方(或容器)提供这些依赖。
本技能并非详尽无遗。使用DI库(google/wire、uber-go/dig、uber-go/fx、samber/do)时,请参考库的官方文档和代码示例以获取当前API签名。
关于基于接口的设计基础(依赖接口,返回结构体),请查看
samber/cc-skills-golang@golang-structs-interfaces
技能。

Best Practices Summary

最佳实践总结

  1. Dependencies MUST be injected via constructors — NEVER use global variables or
    init()
    for service setup
  2. Small projects (< 10 services) SHOULD use manual constructor injection — no library needed
  3. Interfaces MUST be defined where consumed, not where implemented — accept interfaces, return structs
  4. NEVER use global registries or package-level service locators
  5. The DI container MUST only exist at the composition root (
    main()
    or app startup) — NEVER pass the container as a dependency
  6. Prefer lazy initialization — only create services when first requested
  7. Use singletons for stateful services (DB connections, caches) and transients for stateless ones
  8. Mock at the interface boundary — DI makes this trivial
  9. Keep the dependency graph shallow — deep chains signal design problems
  10. Choose the right DI library for your project size and team — see the decision table below
  1. 依赖项必须通过构造函数注入——绝不要使用全局变量或
    init()
    进行服务初始化
  2. 小型项目(<10个服务)应使用手动构造函数注入——无需使用库
  3. 接口必须在消费端定义,而非实现端——依赖接口,返回结构体
  4. 绝不要使用全局注册中心或包级服务定位器
  5. DI容器只能存在于组合根(
    main()
    或应用启动处)——绝不要将容器作为依赖项传递
  6. 优先使用延迟初始化——仅在首次请求时创建服务
  7. 有状态服务使用单例(数据库连接、缓存),无状态服务使用瞬态实例
  8. 在接口边界处进行Mock——DI让这一操作变得简单
  9. 保持依赖图层级较浅——深层依赖链意味着设计存在问题
  10. 为项目规模和团队选择合适的DI库——请查看下方的决策表

Why Dependency Injection?

为什么需要依赖注入?

Problem without DIHow DI solves it
Functions create their own dependenciesDependencies are injected — swap implementations freely
Testing requires real databases, APIsPass mock implementations in tests
Changing one component breaks othersLoose coupling via interfaces — components don't know each other's internals
Services initialized everywhereCentralized container manages lifecycle (singleton, factory, lazy)
All services loaded at startupLazy 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.
无DI时的问题DI的解决方案
函数自行创建依赖项依赖项被注入——可自由替换实现
测试需要真实数据库、API在测试中传递Mock实现
修改一个组件会破坏其他组件通过接口实现松耦合——组件无需了解彼此的内部实现
服务在各处初始化集中式容器管理生命周期(单例、工厂、延迟)
所有服务在启动时加载延迟加载——仅在首次请求时创建服务
全局状态和
init()
函数
在启动时显式连接——可预测、易于调试
DI在拥有大量互联服务的应用中表现出色——HTTP服务器、微服务、带插件的CLI工具。对于只有2-3个函数的小型脚本,手动连接即可。不要过度设计。

Manual Constructor Injection (No Library)

手动构造函数注入(无需库)

For small projects, pass dependencies through constructors. See Manual DI examples for a complete application example.
go
// ✓ 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")
}
go
// ✗ 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}
}
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示例
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("DATABASE_URL")) // 隐藏的依赖项
    return &UserService{db: db}
}
当出现以下情况时,手动DI不再适用:
  • 拥有15个以上存在交叉依赖的服务
  • 需要生命周期管理(健康检查、优雅停机)
  • 想要延迟初始化或作用域容器
  • 连接顺序变得脆弱且难以维护

DI Library Comparison

DI库对比

Go has three main approaches to DI libraries:
  • google/wire examples — Compile-time code generation
  • uber-go/dig + fx examples — Reflection-based framework
  • samber/do examples — Generics-based, no code generation
Go的DI库主要有三种实现方式:
  • google/wire示例 —— 编译时代码生成
  • uber-go/dig + fx示例 —— 基于反射的框架
  • samber/do示例 —— 基于泛型,无需代码生成

Decision Table

决策表

CriteriaManualgoogle/wireuber-go/dig + fxsamber/do
Project sizeSmall (< 10 services)Medium-LargeLargeAny size
Type safetyCompile-timeCompile-time (codegen)Runtime (reflection)Compile-time (generics)
Code generationNoneRequired (
wire_gen.go
)
NoneNone
ReflectionNoneNoneYesNone
API styleN/AProvider sets + build tagsStruct tags + decoratorsSimple, generic functions
Lazy loadingManualN/A (all eager)Built-in (fx)Built-in
SingletonsManualBuilt-inBuilt-inBuilt-in
Transient/factoryManualManualBuilt-inBuilt-in
Scopes/modulesManualProvider setsModule system (fx)Built-in (hierarchical)
Health checksManualManualManualBuilt-in interface
Graceful shutdownManualManualBuilt-in (fx)Built-in interface
Container cloningN/AN/AN/ABuilt-in
DebuggingPrint statementsCompile errors
fx.Visualize()
ExplainInjector()
, web interface
Go versionAnyAnyAny1.18+ (generics)
Learning curveNoneMediumHighLow
评估标准手动注入google/wireuber-go/dig + fxsamber/do
项目规模小型(<10个服务)中大型大型任意规模
类型安全编译时编译时(代码生成)运行时(反射)编译时(泛型)
代码生成需要(
wire_gen.go
反射
API风格N/A提供者集合 + 构建标签结构体标签 + 装饰器简单的泛型函数
延迟加载手动实现不支持(全部立即加载)内置支持(fx)内置支持
单例手动实现内置支持内置支持内置支持
瞬态/工厂手动实现手动实现内置支持内置支持
作用域/模块手动实现提供者集合模块系统(fx)内置支持(分层)
健康检查手动实现手动实现手动实现内置接口
优雅停机手动实现手动实现内置支持(fx)内置接口
容器克隆N/AN/AN/A内置支持
调试打印语句编译错误
fx.Visualize()
ExplainInjector()
、Web界面
Go版本要求任意任意任意1.18+(泛型)
学习曲线中等

Quick Comparison: Same App, Four Ways

快速对比:同一应用的四种实现方式

The dependency graph:
Config -> Database -> UserStore -> UserService -> API
Manual:
go
cfg := NewConfig()
db := NewDatabase(cfg)
store := NewUserStore(db)
svc := NewUserService(store)
api := NewAPI(svc)
api.Run()
// No automatic shutdown, health checks, or lazy loading
google/wire:
go
// wire.go — then run: wire ./...
func InitializeAPI() (*API, error) {
    wire.Build(NewConfig, NewDatabase, NewUserStore, NewUserService, NewAPI)
    return nil, nil
}
// No shutdown or health check support
uber-go/fx:
go
app := fx.New(
    fx.Provide(NewConfig, NewDatabase, NewUserStore, NewUserService),
    fx.Invoke(func(api *API) { api.Run() }),
)
app.Run() // manages lifecycle, but reflection-based
samber/do:
go
i := do.New()
do.Provide(i, NewConfig)
do.Provide(i, NewDatabase)    // auto shutdown + health check
do.Provide(i, NewUserStore)
do.Provide(i, NewUserService)
api := do.MustInvoke[*API](i)
api.Run()
// defer i.Shutdown() — handles all cleanup automatically
依赖图:
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](i)
api.Run()
// defer i.Shutdown() —— 自动处理所有清理工作

Testing with DI

结合DI进行测试

DI makes testing straightforward — inject mocks instead of real implementations:
go
// 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")
    }
}
DI让测试变得简单——注入Mock实现而非真实实现:
go
// 定义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
}

// 手动注入进行测试
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")
    }
}

Testing with samber/do — Clone and Override

结合samber/do进行测试——克隆与覆盖

Container cloning creates an isolated copy where you override only the services you need to mock:
go
func TestUserService_WithDo(t *testing.T) {
    // Create a test injector with mock implementation
    testInjector := do.New()

    // Provide the mock UserStore interface
    do.Override[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
}
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).
容器克隆会创建一个独立的副本,你可以在其中仅覆盖需要Mock的服务:
go
func TestUserService_WithDo(t *testing.T) {
    // 创建带有Mock实现的测试注入器
    testInjector := do.New()

    // 提供Mock UserStore接口
    do.Override[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")
    // ... 断言逻辑
}
这在集成测试中尤其有用,此时你希望大多数服务为真实实现,但需要Mock特定边界(数据库、外部API、邮件服务)。

When to Adopt a DI Library

何时采用DI库

SignalAction
< 10 services, simple dependenciesStay with manual constructor injection
10-20 services, some cross-cutting concernsConsider a DI library
20+ services, lifecycle management neededStrongly recommended
Need health checks, graceful shutdownUse a library with built-in lifecycle support
Team unfamiliar with DI conceptsStart manual, migrate incrementally
信号行动
<10个服务,依赖关系简单继续使用手动构造函数注入
10-20个服务,存在一些横切关注点考虑使用DI库
20+个服务,需要生命周期管理强烈推荐使用DI库
需要健康检查、优雅停机使用内置生命周期支持的库
团队不熟悉DI概念从手动注入开始,逐步迁移

Common Mistakes

常见错误

MistakeFix
Global variables as dependenciesPass through constructors or DI container
init()
for service setup
Explicit initialization in
main()
or container
Depending on concrete typesAccept 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 requestOne container per application; use scopes for request-level isolation
错误修复方案
将全局变量作为依赖项通过构造函数或DI容器传递
使用
init()
进行服务初始化
main()
或容器中显式初始化
依赖具体类型在消费边界处依赖接口
到处传递容器(服务定位器)注入特定依赖项,而非容器本身
深层依赖链(A->B->C->D->E)扁平化——大多数服务应直接依赖仓库和配置
为每个请求创建新容器每个应用使用一个容器;使用作用域实现请求级隔离

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
  • → 查看
    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初始化的放置位置

References

参考资料