golang-dependency-injection

Comprehensive guide for dependency injection (DI) in Golang. Covers why DI matters (testability, loose coupling, separation of concerns, lifecycle management),…

INSTALLATION
npx skills add https://github.com/samber/cc-skills-golang --skill golang-dependency-injection
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

$2c

  • 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 &#x26;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 &#x26;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 := &#x26;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, &#x26;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

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card