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.
## Table of Contents
- [dig — Basic Container](#dig--basic-container)
- [Named Dependencies](#named-dependencies)
- [dig Tradeoffs](#dig-tradeoffs)
- [fx — Full Application Framework](#fx--full-application-framework)
- [Basic Application](#basic-application)
- [Lifecycle Hooks](#lifecycle-hooks)
- [Modules](#modules)
- [Testing with fx](#testing-with-fx)
- [fx Tradeoffs](#fx-tradeoffs)
## 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.
SKILL.md
---
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, Codex or similar harness, and for projects using Golang.
metadata:
author: samber
version: "1.3.1"
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
paths:
- "**/*.go"
---
**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:** Fan out the three sub-agents described in Refactor mode (global/init discovery, concrete-dependency mapping, service-locator detection) when refactoring a large coupled codebase toward dependency injection, and consolidate into one migration plan. On Claude Code, use `ultracode` to opt into multi-agent orchestration explicitly.
**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 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](./references/manual-di.md) 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 Library Comparison
Go has three main approaches to DI libraries:
- [google/wire examples](./references/google-wire.md) — Compile-time code generation
- [uber-go/dig + fx examples](./references/uber-dig-fx.md) — Reflection-based framework
- [samber/do examples](./references/samber-do.md) — Generics-based, no code generation
### 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: Wiring Style
The same graph — `Config -> Database -> UserStore -> UserService -> API` — wired by hand and by a container. The contrast is what the wiring code encodes: an ordered call sequence you maintain, versus a set of providers the container orders for you.
```go
// 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 and uber-go/fx express the same graph differently: wire generates the manual sequence above at build time from a `wire.Build` provider list (cleanup via `func()` returned by providers, no lifecycle hooks), while fx registers providers with `fx.Provide` and resolves them by reflection at runtime with `OnStart`/`OnStop` hooks. Full wiring examples for each: [google/wire](./references/google-wire.md), [uber-go/dig + fx](./references/uber-dig-fx.md), [samber/do](./references/samber-do.md).
## Testing with 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")
}
}
```
### Testing with samber/do — Clone and Override
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.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
| 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 |
| `init()` 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
- [samber/do/v2 documentation](https://do.samber.dev) | [github.com/samber/do/v2](https://github.com/samber/do)
- [google/wire user guide](https://github.com/google/wire/blob/main/docs/guide.md)
- [uber-go/fx documentation](https://uber-go.github.io/fx/)
- [uber-go/dig](https://github.com/uber-go/dig)