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
- 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.
// ✓ 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
| 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: 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
| 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