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:
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-injectionskill takes precedence.
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.
init() for service setupmain() or app startup) — NEVER pass the container as a dependencyProblem without DIHow DI solves itFunctions create their own dependenciesDependencies are injected — swap implementations freelyTesting requires real databases, APIsPass mock implementations in testsChanging one component breaks othersLoose coupling via interfaces — components don't know each other's internalsServices initialized everywhereCentralized container manages lifecycle (singleton, factory, lazy)All services loaded at startupLazy loading — services created only when first requestedGlobal 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.
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:
Go has three main approaches to DI libraries:
CriteriaManualgoogle/wireuber-go/dig + fxsamber/doProject sizeSmall (< 10 services)Medium-LargeLargeAny sizeType safetyCompile-timeCompile-time (codegen)Runtime (reflection)Compile-time (generics)Code generationNoneRequired ( wire_gen.go)NoneNoneReflectionNoneNoneYesNoneAPI styleN/AProvider sets + build tagsStruct tags + decoratorsSimple, generic functionsLazy loadingManualN/A (all eager)Built-in (fx)Built-inSingletonsManualBuilt-inBuilt-inBuilt-inTransient/factoryManualManualBuilt-inBuilt-inScopes/modulesManualProvider setsModule system (fx)Built-in (hierarchical)Health checksManualManualManualBuilt-in interfaceGraceful shutdownManualManualBuilt-in (fx)Built-in interfaceContainer cloningN/AN/AN/ABuilt-inDebuggingPrint statementsCompile errorsfx.Visualize()``ExplainInjector(), web interfaceGo versionAnyAnyAny1.18+ (generics)Learning curveNoneMediumHighLow
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 lifecycle hooks (OnStart/OnStop) or health checks; cleanup via returned func() from providers
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
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")
}
}
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).
SignalAction< 10 services, simple dependenciesStay with manual constructor injection10-20 services, some cross-cutting concernsConsider a DI library20+ services, lifecycle management neededStrongly recommendedNeed health checks, graceful shutdownUse a library with built-in lifecycle supportTeam unfamiliar with DI conceptsStart manual, migrate incrementally
MistakeFixGlobal variables as dependenciesPass through constructors or DI containerinit() for service setupExplicit initialization in main() or containerDepending on concrete typesAccept interfaces at consumption boundariesPassing the container everywhere (service locator)Inject specific dependencies, not the containerDeep dependency chains (A->B->C->D->E)Flatten — most services should depend on repositories and config directlyCreating a new container per requestOne container per application; use scopes for request-level isolation
samber/cc-skills-golang@golang-samber-do skill for detailed samber/do usage patternssamber/cc-skills-golang@golang-structs-interfaces skill for interface design and compositionsamber/cc-skills-golang@golang-testing skill for testing with dependency injectionsamber/cc-skills-golang@golang-project-layout skill for DI initialization placement