Dependency Injection
Go has no built-in DI framework like .NET — dependency injection is explicit constructor injection.
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.
sequenceDiagramClient->>Server: HTTP RequestServer->>Handler: ServeHTTPHandler->>Service: Business LogicService->>DB: QueryDB-->>Handler: DataHandler-->>Client: JSON Response
Step-by-step explanation
- Define Repository interface in service package.
- PostgresRepo implements Repository with *sql.DB.
- Service struct holds Repository interface field.
- NewService injects repo via constructor parameter.
- Handler holds *Service; main wires: repo → svc → handler.
- Tests pass mockRepository to NewService.
Practical code example
Manual DI wiring handler, service, and repository in main:
package mainimport ("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) *PaymentServiceis the constructor injector.return &PaymentService{repo: repo, log: log}assigns dependencies at creation time.interface Repositoryallows injectingPostgresRepoin prod andMemRepoin 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 pattern—NewServer(WithLogger(l), WithDB(db))for optional deps.google/wiregenerates 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?
Q2BeginnerWhy inject interfaces?
Q3IntermediateWire vs manual DI?
Q4IntermediateComposition root?
Q5AdvancedTest HTTP handler with mocked service.
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.