Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 38

    Dependency Injection

    Go has no built-in DI framework like .NET — dependency injection is explicit constructor injection.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Go has no built-in DI framework like .NET — dependency injection is explicit constructor injection. Pass dependencies as struct fields set in constructor functions; accept interfaces for testability. Wire, fx, and dig provide compile-time or runtime containers for larger apps.

    Clean architecture in Go: main.go wires concrete implementations to handlers; services depend on repository interfaces; tests inject mocks. Interviewers ask how you test HTTP handlers and swap database implementations without frameworks.

    This lesson wires handler → service → repository manually — the pattern used in most Go microservices before reaching for Wire code generation.

    The story

    Uber's Go microservices wire dependencies in main: construct a database pool, pass it to a repository, inject the repository into a service, and attach the service to HTTP handlers. Constructor functions like NewPaymentService(repo Repository, logger *slog.Logger) make dependencies explicit — no hidden globals, easy to swap fakes in tests.

    Frameworks like Wire (Google) codegen dependency graphs, but manual constructor injection remains the most readable pattern for small and medium services.

    Understanding the topic

    Key concepts

    • Constructor injection: NewService(repo Repository) *Service.
    • Depend on interfaces defined by consumer package.
    • main.go composition root — only place knowing all concretes.
    • google/wire compile-time DI code generation.
    • uber/fx runtime DI with lifecycle hooks.
    • Avoid global singletons — pass dependencies explicitly.
    text
    sequenceDiagram
    Client->>Server: HTTP Request
    Server->>Handler: ServeHTTP
    Handler->>Service: Business Logic
    Service->>DB: Query
    DB-->>Handler: Data
    Handler-->>Client: JSON Response

    Step-by-step explanation

    1. Define Repository interface in service package.
    2. PostgresRepo implements Repository with *sql.DB.
    3. Service struct holds Repository interface field.
    4. NewService injects repo via constructor parameter.
    5. Handler holds *Service; main wires: repo → svc → handler.
    6. Tests pass mockRepository to NewService.

    Practical code example

    Manual DI wiring handler, service, and repository in main:

    go
    package main
    import (
    "context"
    "fmt"
    "net/http"
    )
    type UserRepository interface {
    FindByID(ctx context.Context, id string) (string, error)
    }
    type postgresUserRepo struct{}
    func (postgresUserRepo) FindByID(ctx context.Context, id string) (string, error) {
    return "Alice", nil
    }
    type UserService struct {
    repo UserRepository
    }
    func NewUserService(repo UserRepository) *UserService {
    return &UserService{repo: repo}
    }
    func (s *UserService) GetName(ctx context.Context, id string) (string, error) {
    return s.repo.FindByID(ctx, id)
    }
    type UserHandler struct {
    svc *UserService
    }
    func NewUserHandler(svc *UserService) *UserHandler {
    return &UserHandler{svc: svc}
    }
    func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    name, err := h.svc.GetName(r.Context(), r.URL.Query().Get("id"))
    if err != nil {
    http.Error(w, err.Error(), 500)
    return
    }
    fmt.Fprintln(w, name)
    }
    func main() {
    repo := postgresUserRepo{}
    svc := NewUserService(repo)
    handler := NewUserHandler(svc)
    http.ListenAndServe(":8080", handler)
    }

    Line-by-line code explanation

    • type PaymentService struct { repo Repository; log *slog.Logger } declares dependencies as fields.
    • func NewPaymentService(repo Repository, log *slog.Logger) *PaymentService is the constructor injector.
    • return &PaymentService{repo: repo, log: log} assigns dependencies at creation time.
    • interface Repository allows injecting PostgresRepo in prod and MemRepo in tests.
    • main() is the composition root — the only place that wires concrete implementations together.
    • avoid global singletons — they hide dependencies and make parallel tests flaky.
    • functional options patternNewServer(WithLogger(l), WithDB(db)) for optional deps.
    • google/wire generates wiring code from provider functions — scales in large monorepos.

    Key takeaway: main is composition root. Service accepts interface — swap postgresUserRepo for mock in tests. No global state.

    Real-world use

    Where you'll use this in production

    • Swapping Postgres for in-memory repo in unit tests.
    • Multi-implementation payment processors (Stripe, PayPal).
    • Feature flag injected config service.
    • Wire-generated wiring in large monorepos.

    Best practices

    • Constructor functions for every injectable type.
    • Interface at consumer, implementation in infrastructure package.
    • Keep main.go as only composition root.
    • Avoid service locator and global variables.
    • Use Wire when manual wiring exceeds ~20 lines.
    • Document lifecycle: who closes DB connections.

    Common mistakes

    • Concrete types everywhere — untestable handlers.
    • Init() functions creating global DB connection.
    • Circular dependencies between packages.
    • Over-using DI framework for 3-component app.
    • Interface on wrong side — producer defines giant interface.

    Advanced interview questions

    Q1BeginnerDI in Go without Spring?
    Constructor injection; interfaces; main wires concretes.
    Q2BeginnerWhy inject interfaces?
    Swap implementations in tests; decouple layers.
    Q3IntermediateWire vs manual DI?
    Wire generates wiring code compile-time; manual fine for small apps.
    Q4IntermediateComposition root?
    main package — only place importing all layers and constructing graph.
    Q5AdvancedTest HTTP handler with mocked service.
    Mock UserRepository; NewUserService(mock); httptest against NewUserHandler(svc).

    Summary

    Go DI is explicit constructor injection via interfaces. main.go is the composition root wiring all dependencies. Accept interfaces in services; return concrete from constructors. Wire/fx help scale DI; manual wiring fine for small services. Next lesson: configuration management.

    Ready to mark this lesson complete?Track your journey across the entire course.