返回 Skills
samber/cc-skills-golang· MIT 内容可用

golang-dependency-injection

Comprehensive guide for dependency injection (DI) in Golang. Covers why DI matters (testability, loose coupling, separation of concerns, lifecycle management), manual constructor injection, and DI library comparison (google/wire, uber-go/dig, uber-go/fx, samber/do). Use this skill when designing service architecture, setting up dependency injection, refactoring tightly coupled code, managing singletons or service factories, or when the user asks about inversion of control, service containers, or wiring dependencies in Go. For a specific DI library, → See `samber/cc-skills-golang@golang-google-wire`, `samber/cc-skills-golang@golang-uber-dig`, `samber/cc-skills-golang@golang-uber-fx`, or `samber/cc-skills-golang@golang-samber-do` skills.

安装

与 skills.sh 相同的 Command / Prompt 安装方式


name: golang-dependency-injection description: "Comprehensive guide for dependency injection (DI) in Golang. Covers why DI matters (testability, loose coupling, separation of concerns, lifecycle management), manual constructor injection, and DI library comparison (google/wire, uber-go/dig, uber-go/fx, samber/do). Use this skill when designing service architecture, setting up dependency injection, refactoring tightly coupled code, managing singletons or service factories, or when the user asks about inversion of control, service containers, or wiring dependencies in Go. For a specific DI library, → See samber/cc-skills-golang@golang-google-wire, samber/cc-skills-golang@golang-uber-dig, samber/cc-skills-golang@golang-uber-fx, or samber/cc-skills-golang@golang-samber-do skills." user-invocable: true license: MIT compatibility: Designed for Claude Code or similar AI coding agents, and for projects using Golang. metadata: author: samber version: "1.2.2" openclaw: emoji: "🔌" homepage: https://github.com/samber/cc-skills-golang requires: bins: - go install: [] allowed-tools: Read Edit Write Glob Grep Bash(go:) Bash(golangci-lint:) Bash(git:*) Agent WebFetch mcp__context7__resolve-library-id mcp__context7__query-docs AskUserQuestion

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.

Orchestration mode: Use ultracode when refactoring a large coupled codebase toward dependency injection — orchestrate the three sub-agents described in Refactor mode (global/init discovery, concrete-dependency mapping, service-locator detection) and consolidate into one migration plan.

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
  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

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() functionsExplicit 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.

// ✓ 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}
}

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

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 errorsfx.Visualize()ExplainInjector(), web interface
Go versionAnyAnyAny1.18+ (generics)
Learning curveNoneMediumHighLow

Quick Comparison: Same App, Four Ways

The dependency graph: Config -> Database -> UserStore -> UserService -> API

Manual:

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:

// wire.go — then run: wire ./...
func InitializeAPI() (*API, error) {
    wire.Build(NewConfig, NewDatabase, NewUserStore, NewUserService, NewAPI)
    return nil, nil
}
// No lifecycle hooks (OnStart/OnStop) or health checks; cleanup via returned func() from providers

uber-go/fx:

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:

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

Testing with DI

DI makes testing straightforward — inject mocks instead of real implementations:

// 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")
    }
}

Testing with samber/do — Clone and Override

Container cloning creates an isolated copy where you override only the services you need to mock:

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
}

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

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

Common Mistakes

MistakeFix
Global variables as dependenciesPass through constructors or DI container
init() for service setupExplicit 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

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

附带文件

evals/evals.json
[
  {
    "id": 1,
    "name": "constructor-injection-not-globals",
    "description": "Tests that dependencies are injected via constructors, not global variables or init()",
    "prompt": "I have a UserService that needs a database connection and a logger. What's the best way to set this up in Go? I was thinking of using a package-level var for the database.",
    "trap": "Without the skill, the model may accept the global variable approach or use init() for setup. The skill explicitly forbids globals and init() for service setup.",
    "assertions": [
      {"id": "1.1", "text": "Uses constructor injection (NewUserService taking dependencies as parameters)"},
      {"id": "1.2", "text": "Explicitly advises against package-level variables for service dependencies"},
      {"id": "1.3", "text": "Explains why globals are problematic (untestable, hidden dependencies, or coupling)"},
      {"id": "1.4", "text": "Does NOT use init() for service initialization"},
      {"id": "1.5", "text": "Returns a concrete struct pointer from the constructor, not an interface"}
    ]
  },
  {
    "id": 2,
    "name": "interface-defined-at-consumer",
    "description": "Tests that interfaces are defined where consumed, not where implemented",
    "prompt": "I'm building a Go service that uses a UserStore. Should I define the UserStore interface in the same package as the PostgreSQL implementation or somewhere else? Show me the correct pattern.",
    "trap": "Without the skill, the model often defines the interface in the implementation package (next to the struct). The skill requires interfaces to be defined at the consumption site.",
    "assertions": [
      {"id": "2.1", "text": "Defines the interface in the consuming package (e.g. service package), not the implementation package"},
      {"id": "2.2", "text": "Explains the principle: accept interfaces, return structs"},
      {"id": "2.3", "text": "The implementation package returns a concrete struct pointer"},
      {"id": "2.4", "text": "The consumer depends on its own locally-defined interface"},
      {"id": "2.5", "text": "Does NOT have the implementation package import the consumer's interface"}
    ]
  },
  {
    "id": 3,
    "name": "container-not-passed-as-dependency",
    "description": "Tests that the DI container is never passed as a dependency (service locator anti-pattern)",
    "prompt": "I'm using samber/do for DI in my Go project. My UserService needs a Database and a Mailer. Should I pass the do.Injector to UserService so it can look up what it needs?",
    "trap": "Without the skill, the model may accept passing the injector/container as a dependency. The skill explicitly forbids this as the service locator anti-pattern.",
    "assertions": [
      {"id": "3.1", "text": "Advises against passing the injector/container as a dependency"},
      {"id": "3.2", "text": "Identifies this as the service locator anti-pattern"},
      {"id": "3.3", "text": "Shows that the Injector should only exist at the composition root (main or app startup)"},
      {"id": "3.4", "text": "Shows UserService receiving Database and Mailer directly as constructor parameters"},
      {"id": "3.5", "text": "Shows the provider function using do.MustInvoke inside the provider, not inside UserService methods"}
    ]
  },
  {
    "id": 4,
    "name": "manual-di-for-small-projects",
    "description": "Tests that small projects use manual DI, not a library",
    "prompt": "I'm building a small Go REST API with about 5 services: config, database, user repository, user service, and HTTP handler. What DI approach should I use?",
    "trap": "Without the skill, the model may recommend a DI library like Wire or Fx for any project. The skill says small projects (< 10 services) should use manual constructor injection.",
    "assertions": [
      {"id": "4.1", "text": "Recommends manual constructor injection for a project with only 5 services"},
      {"id": "4.2", "text": "Does NOT recommend a DI library as the primary approach"},
      {"id": "4.3", "text": "Shows wiring in main() with explicit constructor calls in dependency order"},
      {"id": "4.4", "text": "Initializes infrastructure first, then repositories, then services, then transport"},
      {"id": "4.5", "text": "Mentions that a DI library becomes worthwhile at 10-20+ services"}
    ]
  },
  {
    "id": 5,
    "name": "di-library-selection-judgment",
    "description": "Tests correct DI library recommendation based on project characteristics",
    "prompt": "I'm building a large Go microservice with 40+ services, complex lifecycle management (health checks, graceful shutdown), and I want compile-time type safety. My team is comfortable with Go generics. Which DI library should I use?",
    "trap": "Without the skill, the model may recommend uber-go/fx (most popular) or google/wire. The skill's decision table shows samber/do matches all these criteria: any size, compile-time generics, built-in lifecycle, health checks, shutdown.",
    "assertions": [
      {"id": "5.1", "text": "Recommends samber/do as a strong fit given the criteria (generics, lifecycle, compile-time safety)"},
      {"id": "5.2", "text": "Explains why uber-go/fx is a valid alternative but uses reflection (runtime errors, not compile-time)"},
      {"id": "5.3", "text": "Explains why google/wire lacks built-in lifecycle management (no health checks, no shutdown)"},
      {"id": "5.4", "text": "Mentions that samber/do requires Go 1.18+ for generics"},
      {"id": "5.5", "text": "Discusses at least 3 DI library options from the decision table"}
    ]
  },
  {
    "id": 6,
    "name": "wire-build-constraint-and-codegen",
    "description": "Tests proper google/wire setup with wireinject build constraint",
    "prompt": "Set up google/wire for a Go application with Config, Database, UserStore, and UserService. Show the wire.go file and explain what happens when I run wire.",
    "trap": "Without the skill, the model may forget the //go:build wireinject build constraint or not explain that wire.Build generates plain Go constructors.",
    "assertions": [
      {"id": "6.1", "text": "Includes //go:build wireinject build constraint in the wire.go file"},
      {"id": "6.2", "text": "Uses wire.Build with all provider functions listed"},
      {"id": "6.3", "text": "Shows wire.Bind for binding interface to implementation"},
      {"id": "6.4", "text": "Explains that wire generates wire_gen.go with plain constructor calls"},
      {"id": "6.5", "text": "Mentions that wire_gen.go must not be edited manually"}
    ]
  },
  {
    "id": 7,
    "name": "fx-lifecycle-hooks-pattern",
    "description": "Tests proper uber-go/fx lifecycle hook usage for startup/shutdown",
    "prompt": "Set up a database connection using uber-go/fx that connects on startup and cleanly closes on shutdown.",
    "trap": "Without the skill, the model may open and close the connection manually instead of using fx.Lifecycle hooks. The skill requires OnStart/OnStop hooks.",
    "assertions": [
      {"id": "7.1", "text": "Uses fx.Lifecycle parameter in the provider function"},
      {"id": "7.2", "text": "Registers OnStart hook for establishing the database connection"},
      {"id": "7.3", "text": "Registers OnStop hook for closing the database connection"},
      {"id": "7.4", "text": "Uses lc.Append(fx.Hook{...}) pattern"},
      {"id": "7.5", "text": "OnStart and OnStop take context.Context as parameter"}
    ]
  },
  {
    "id": 8,
    "name": "testing-with-di-mock-injection",
    "description": "Tests that DI enables testing by injecting mocks at the interface boundary",
    "prompt": "Write a test for a UserService that depends on a UserStore interface. The test should verify that GetUser returns the correct user when found and an error when not found. Don't use any DI library.",
    "trap": "Without the skill, the model may test with a real database or skip the mock pattern. The skill requires mocking at the interface boundary.",
    "assertions": [
      {"id": "8.1", "text": "Creates a mock implementation of the UserStore interface"},
      {"id": "8.2", "text": "Injects the mock into UserService via the constructor (NewUserService)"},
      {"id": "8.3", "text": "Tests both the success path (user found) and the error path (not found)"},
      {"id": "8.4", "text": "Does NOT use a real database connection in the test"},
      {"id": "8.5", "text": "The mock is defined in the test file, not as a package-level or global variable"}
    ]
  },
  {
    "id": 9,
    "name": "shallow-dependency-graph",
    "description": "Tests that deep dependency chains are flagged as a design problem",
    "prompt": "My Go application has this dependency chain: Config -> Database -> UserRepo -> UserService -> NotificationService -> OrderService -> PaymentService -> APIHandler. Is this a good architecture?",
    "trap": "Without the skill, the model may accept this deep chain as normal layered architecture. The skill says deep chains signal design problems and recommends flattening.",
    "assertions": [
      {"id": "9.1", "text": "Identifies the deep dependency chain as a design problem"},
      {"id": "9.2", "text": "Recommends flattening the dependency graph"},
      {"id": "9.3", "text": "Suggests that most services should depend on repositories and config directly, not transitively through other services"},
      {"id": "9.4", "text": "Explains the negative consequences of deep chains (fragility, hard to test, or hard to maintain)"},
      {"id": "9.5", "text": "Proposes a concrete restructuring where OrderService and PaymentService don't depend on each other transitively"}
    ]
  },
  {
    "id": 10,
    "name": "one-container-per-app-not-per-request",
    "description": "Tests that a DI container is created once per application, not per request",
    "prompt": "I'm building a Go HTTP API with samber/do. In each HTTP handler, I'm creating a new do.New() injector, providing all services, then invoking the one I need. This way each request gets fresh services. Is this correct?",
    "trap": "Without the skill, the model may accept the per-request container pattern as reasonable isolation. The skill explicitly forbids creating a new container per request.",
    "assertions": [
      {"id": "10.1", "text": "Identifies creating a new container per request as a mistake"},
      {"id": "10.2", "text": "Recommends one container per application created at startup"},
      {"id": "10.3", "text": "Explains the performance or correctness problem with per-request containers (recreating singletons, no connection reuse)"},
      {"id": "10.4", "text": "Suggests using scopes for request-level isolation if needed"},
      {"id": "10.5", "text": "Shows the container being created once in main() and services injected into handlers"}
    ]
  },
  {
    "id": 11,
    "name": "lazy-vs-eager-initialization",
    "description": "Tests knowledge of lazy initialization preference and singleton vs transient distinction",
    "prompt": "When using a DI container in Go, should all my services be created at application startup? I have a database pool, a cache client, and some request processing services.",
    "trap": "Without the skill, the model may recommend eager initialization for everything. The skill prefers lazy initialization and distinguishes singletons for stateful services from transients for stateless ones.",
    "assertions": [
      {"id": "11.1", "text": "Recommends lazy initialization (services created on first use, not all at startup)"},
      {"id": "11.2", "text": "Recommends singletons for stateful services like database connections and cache clients"},
      {"id": "11.3", "text": "Recommends transients (or factories) for stateless request processing services"},
      {"id": "11.4", "text": "Explains why lazy loading is beneficial (unused services are never created, faster startup)"},
      {"id": "11.5", "text": "Notes which DI libraries support lazy loading (samber/do, fx) vs which don't (wire is all eager)"}
    ]
  }
]
references/google-wire.md
# google/wire — Compile-Time Code Generation

Wire uses code generation to resolve the dependency graph at compile time. Type-safe, but requires a build step.

- Docs: [github.com/google/wire](https://github.com/google/wire) | [User Guide](https://github.com/google/wire/blob/main/docs/guide.md)

Before writing Wire code, refer to the library's official documentation for up-to-date API signatures and examples.

## Provider Definitions

```go
// providers.go
package wire

import "github.com/google/wire"

// ProviderSet groups related providers
var InfraSet = wire.NewSet(
    NewConfig,
    NewDatabase,
    NewCache,
)

var ServiceSet = wire.NewSet(
    NewUserService,
    wire.Bind(new(UserStore), new(*PostgresUserStore)), // bind interface to impl
)
```

## Injector Definition

```go
// wire.go — build constraint ensures this is only used by the wire tool
//go:build wireinject

package main

import "github.com/google/wire"

func InitializeApp() (*App, error) {
    wire.Build(
        InfraSet,
        ServiceSet,
        NewApp,
    )
    return nil, nil // wire replaces this body
}
```

## Generated Code

Run `wire ./...` to produce `wire_gen.go`:

```go
// wire_gen.go — DO NOT EDIT (auto-generated by wire)
func InitializeApp() (*App, error) {
    config := NewConfig()
    database, err := NewDatabase(config)
    if err != nil {
        return nil, err
    }
    cache := NewCache(config)
    store := NewPostgresUserStore(database)
    userService := NewUserService(store, cache)
    app := NewApp(userService)
    return app, nil
}
```

## Testing

Wire generates plain constructors, so testing uses manual injection — no container to clone:

```go
func TestUserService(t *testing.T) {
    mock := &MockUserStore{...}
    svc := NewUserService(mock, NewTestCache())
    // ... test
}
```

## Tradeoffs

- Errors caught at compile time (codegen fails if graph is incomplete)
- Requires running `wire ./...` after every dependency change
- No lazy loading — all dependencies created eagerly
- No built-in lifecycle management (health checks, shutdown)
- No runtime container — wire generates plain Go constructor calls
- Interface bindings require explicit `wire.Bind` declarations
- Generated files (`wire_gen.go`) must be committed and kept in sync

Wire injectors MUST use `//go:build wireinject` build constraint. Generated `wire_gen.go` MUST NOT be edited manually — always regenerate with `wire ./...`.
references/manual-di.md
# Manual Constructor Injection

Manual DI is the simplest approach — pass dependencies through constructors. No library, no magic.

## Complete Application Example

```go
func main() {
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    defer stop()

    // Layer 1: Configuration
    cfg := LoadConfig()
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    // Layer 2: Infrastructure
    db, err := postgres.Connect(cfg.DatabaseURL)
    if err != nil {
        logger.Error("database connection failed", "error", err)
        os.Exit(1)
    }
    defer db.Close()

    cache := redis.NewClient(cfg.RedisURL)
    defer cache.Close()

    mailer := smtp.NewMailer(cfg.SMTPAddr)

    // Layer 3: Repositories
    userRepo := postgres.NewUserRepository(db)
    orderRepo := postgres.NewOrderRepository(db)

    // Layer 4: Services
    userSvc := service.NewUserService(userRepo, cache, mailer, logger)
    orderSvc := service.NewOrderService(orderRepo, userSvc, logger)
    paymentSvc := service.NewPaymentService(orderRepo, cfg.StripeKey, logger)

    // Layer 5: Transport
    handler := http.NewHandler(userSvc, orderSvc, paymentSvc, logger)
    server := http.NewServer(cfg.Port, handler)

    // Run
    go server.ListenAndServe()
    <-ctx.Done()
    server.Shutdown(context.Background())
}
```

## When Manual DI Works Well

- Small to medium projects (< 15 services)
- Simple dependency graph with clear layering
- No need for lazy loading or lifecycle management
- Team prefers explicit, visible wiring

## When Manual DI Breaks Down

- Adding a new service means editing `main()` and getting the wiring order right
- Lifecycle management (health checks, graceful shutdown) must be hand-coded with `defer`
- No lazy initialization — all services are created at startup, even if unused
- Cross-cutting concerns (logging, tracing) must be threaded through every constructor
- With 30+ services, the wiring code becomes fragile and hard to maintain

Manual DI SHOULD be the default for small projects (< 15 services). Dependencies MUST be initialized in order — infrastructure first, then repositories, then services, then transport.
references/samber-do.md
# samber/do — Generics-Based DI

> **For the full samber/do API, patterns, and advanced features, see the `samber/cc-skills-golang@golang-samber-do` skill.**

Type-safe dependency injection using Go generics. No reflection, no code generation, simple API.

- Docs: [do.samber.dev](https://do.samber.dev) | [github.com/samber/do/v2](https://github.com/samber/do)

## Core Pattern

```go
// Register services with providers
injector := do.New()
do.Provide(injector, func(i do.Injector) (*UserService, error) {
    db := do.MustInvoke[*Database](i)
    return NewUserService(db), nil
})

// Invoke services (lazy — created on demand)
svc := do.MustInvoke[*UserService](injector)

// Graceful shutdown — all services implementing Shutdowner are closed
injector.ShutdownOnSignalsWithContext(ctx, os.Interrupt)
```

## Why samber/do

- **No code generation** — no build step, no generated files to maintain
- **No reflection** — errors are caught at compile time via generics, not at runtime
- **Strongly typed** — Go generics provide full type safety without `interface{}` casts
- **Built-in lifecycle** — health checks and graceful shutdown detected automatically
- **Container cloning** — create isolated test containers from production configuration
- **Simple API** — `Provide`, `Invoke`, `Shutdown` — that's most of what you need
- **Package system** — organize services by domain without manual wiring order

→ See `samber/cc-skills-golang@golang-samber-do` for full application setup, package organization, lifecycle management, debugging, testing with clone + override, and complete API reference.
references/uber-dig-fx.md
# uber-go/dig + uber-go/fx — Reflection-Based DI

`dig` is the low-level DI container; `fx` is the full application framework built on top. Powerful but uses reflection — errors appear at startup, not compile time.

- Docs: [github.com/uber-go/dig](https://github.com/uber-go/dig) | [uber-go.github.io/fx](https://uber-go.github.io/fx/)

Before writing dig/fx code, refer to the library's official documentation for up-to-date API signatures and examples.

## dig — Basic Container

```go
func main() {
    container := dig.New()

    container.Provide(NewConfig)
    container.Provide(NewDatabase)
    container.Provide(NewUserStore)
    container.Provide(NewUserService)

    // Invoke — dig resolves the full dependency chain
    err := container.Invoke(func(svc *UserService) {
        svc.Run()
    })
    if err != nil {
        log.Fatal(err)
    }
}
```

### Named Dependencies

```go
type DatabaseParams struct {
    dig.In

    Primary *sql.DB `name:"primary"`
    Replica *sql.DB `name:"replica"`
}

container.Provide(NewPrimaryDB, dig.Name("primary"))
container.Provide(NewReplicaDB, dig.Name("replica"))

container.Provide(func(p DatabaseParams) *UserService {
    return &UserService{
        writer: p.Primary,
        reader: p.Replica,
    }
})
```

### dig Tradeoffs

- Uses reflection — type mismatches are runtime errors, not compile errors
- `dig.In` and `dig.Out` structs add boilerplate for complex graphs
- No built-in lifecycle management
- Powerful grouping with `dig.Group` for collecting multiple implementations

## fx — Full Application Framework

### Basic Application

```go
func main() {
    app := fx.New(
        fx.Provide(
            NewConfig,
            NewDatabase,
            NewUserStore,
            NewUserService,
        ),
        fx.Invoke(RegisterRoutes),
        fx.Invoke(StartServer),
    )

    app.Run() // blocks until signal, then calls shutdown hooks
}
```

### Lifecycle Hooks

```go
func NewDatabase(lc fx.Lifecycle, cfg *Config) (*Database, error) {
    db := &Database{}

    lc.Append(fx.Hook{
        OnStart: func(ctx context.Context) error {
            return db.Connect(cfg.URL)
        },
        OnStop: func(ctx context.Context) error {
            return db.Close()
        },
    })

    return db, nil
}
```

### Modules

```go
var InfraModule = fx.Module("infra",
    fx.Provide(NewConfig),
    fx.Provide(NewDatabase),
    fx.Provide(NewCache),
)

var ServiceModule = fx.Module("service",
    fx.Provide(NewUserService),
    fx.Provide(NewOrderService),
)

app := fx.New(InfraModule, ServiceModule, fx.Invoke(StartServer))
```

### Testing with fx

```go
func TestUserService(t *testing.T) {
    var svc *UserService

    app := fxtest.New(t,
        fx.Provide(NewMockUserStore),
        fx.Provide(NewUserService),
        fx.Populate(&svc),
    )
    app.RequireStart()
    defer app.RequireStop()

    // ... test svc
}
```

### fx Tradeoffs

- Full application framework — manages startup, shutdown, and signal handling
- Reflection-based — errors at startup, not compile time
- Steep learning curve — `fx.In`, `fx.Out`, `fx.Annotate`, `fx.Decorate`
- Built-in lifecycle (OnStart/OnStop hooks)
- Heavyweight — pulls in the full fx framework
- `fxtest` package for testing, but requires starting/stopping the app

fx lifecycle hooks MUST be used for start/stop — register `OnStart`/`OnStop` via `fx.Lifecycle`. fx modules SHOULD group related providers — use `fx.Module` to organize by domain.
    golang-dependency-injection | Prompt Minder