Files
cv-site/doc/_go-learning/patterns/05-dependency-injection.md
T
juanatsap d95c62bad4 refactor: remove outdated server design documentation
Remove 557-line server-design.md from _go-learning/architecture - content is now covered in updated architecture documentation with real implementation examples and test coverage.
2025-12-02 20:25:05 +00:00

12 KiB

Dependency Injection Pattern in Go

Pattern Overview

Dependency Injection (DI) is a pattern where dependencies are provided to a component rather than the component creating them itself. In Go, this is typically done through constructor functions that accept dependencies as parameters.

Pattern Structure

// Define dependencies as interfaces (optional but recommended)
type Database interface {
    Query(query string) (Result, error)
}

// Component accepts dependencies via constructor
type Service struct {
    db     Database
    logger Logger
    config *Config
}

// Constructor injects dependencies
func NewService(db Database, logger Logger, config *Config) *Service {
    return &Service{
        db:     db,
        logger: logger,
        config: config,
    }
}

Real Implementation from Project

Handler with Dependencies

// internal/handlers/cv.go

// CVHandler handles CV-related HTTP requests
type CVHandler struct {
    tmpl *templates.Manager  // Injected template manager
    host string              // Injected host configuration
}

// NewCVHandler creates a new CV handler with injected dependencies
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
    return &CVHandler{
        tmpl: tmpl,
        host: host,
    }
}

// Methods use injected dependencies
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
    // Use injected template manager
    if err := h.tmpl.Render(w, "index.html", data); err != nil {
        // ...
    }

    // Use injected host for absolute URLs
    canonicalURL := fmt.Sprintf("http://%s/", h.host)
}

Template Manager with Dependencies

// internal/templates/manager.go

// Manager handles template rendering
type Manager struct {
    templates map[string]*template.Template
    config    *config.TemplateConfig  // Injected configuration
    mu        sync.RWMutex
}

// NewManager creates template manager with injected config
func NewManager(config *config.TemplateConfig) (*Manager, error) {
    m := &Manager{
        templates: make(map[string]*template.Template),
        config:    config,  // Store injected config
    }

    // Use config to load templates
    if err := m.loadTemplates(); err != nil {
        return nil, err
    }

    return m, nil
}

// Methods use injected config
func (m *Manager) loadTemplates() error {
    // Use injected config
    files, err := filepath.Glob(m.config.Dir + "/*.html")
    // ...
}

Main Function - Wiring Dependencies

// main.go

func main() {
    // Load configuration
    cfg := config.Load()

    // Create template manager (with config dependency)
    tmplManager, err := templates.NewManager(cfg.Templates)
    if err != nil {
        log.Fatal(err)
    }

    // Create handlers (with template manager dependency)
    cvHandler := handlers.NewCVHandler(tmplManager, cfg.Server.Host)
    healthHandler := handlers.NewHealthHandler()

    // Setup routes (with handler dependencies)
    handler := routes.Setup(cvHandler, healthHandler)

    // Start server
    server := &http.Server{
        Addr:    cfg.Server.Port,
        Handler: handler,
    }

    log.Printf("Server starting on %s", cfg.Server.Port)
    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

Benefits of Dependency Injection

1. Testability

// Without DI: Hard to test
type Handler struct {
    // Creates dependencies internally
}

func NewHandler() *Handler {
    db := database.Connect("prod-db")  // Can't mock!
    return &Handler{db: db}
}

// With DI: Easy to test
type Handler struct {
    db Database  // Interface
}

func NewHandler(db Database) *Handler {
    return &Handler{db: db}
}

// Test with mock
func TestHandler(t *testing.T) {
    mockDB := &MockDatabase{}
    handler := NewHandler(mockDB)
    // Test with mock
}

2. Flexibility

// Switch implementations without changing handler code

// Production
realDB := &PostgresDB{conn: conn}
handler := NewHandler(realDB)

// Testing
mockDB := &MockDB{}
handler := NewHandler(mockDB)

// Development
localDB := &SQLiteDB{path: "dev.db"}
handler := NewHandler(localDB)

3. Explicit Dependencies

// Clear what a component needs
func NewService(
    db Database,
    cache Cache,
    logger Logger,
    config *Config,
) *Service {
    // Dependencies are explicit and visible
    return &Service{
        db:     db,
        cache:  cache,
        logger: logger,
        config: config,
    }
}

Constructor Patterns

1. Simple Constructor

// Direct initialization
func NewHandler(tmpl *templates.Manager, host string) *Handler {
    return &Handler{
        tmpl: tmpl,
        host: host,
    }
}

2. Constructor with Validation

// Validate dependencies
func NewHandler(tmpl *templates.Manager, host string) (*Handler, error) {
    if tmpl == nil {
        return nil, errors.New("template manager is required")
    }
    if host == "" {
        return nil, errors.New("host is required")
    }

    return &Handler{
        tmpl: tmpl,
        host: host,
    }, nil
}

3. Constructor with Options

// Options pattern for many optional dependencies
type HandlerOptions struct {
    Host     string
    Timeout  time.Duration
    MaxRetries int
}

func NewHandler(tmpl *templates.Manager, opts *HandlerOptions) *Handler {
    // Apply defaults
    if opts == nil {
        opts = &HandlerOptions{
            Host:       "localhost:8080",
            Timeout:    30 * time.Second,
            MaxRetries: 3,
        }
    }

    return &Handler{
        tmpl:       tmpl,
        host:       opts.Host,
        timeout:    opts.Timeout,
        maxRetries: opts.MaxRetries,
    }
}

4. Functional Options

// Functional options pattern
type HandlerOption func(*Handler)

func WithTimeout(d time.Duration) HandlerOption {
    return func(h *Handler) {
        h.timeout = d
    }
}

func WithLogger(logger Logger) HandlerOption {
    return func(h *Handler) {
        h.logger = logger
    }
}

func NewHandler(tmpl *templates.Manager, opts ...HandlerOption) *Handler {
    h := &Handler{
        tmpl:    tmpl,
        timeout: 30 * time.Second,  // Default
    }

    // Apply options
    for _, opt := range opts {
        opt(h)
    }

    return h
}

// Usage
handler := NewHandler(
    tmplManager,
    WithTimeout(10*time.Second),
    WithLogger(logger),
)

Interface-Based DI

Define Interfaces

// Define interface for dependencies
type TemplateRenderer interface {
    Render(w io.Writer, name string, data interface{}) error
}

type DataLoader interface {
    LoadCV(lang string) (*CV, error)
    LoadUI(lang string) (*UI, error)
}

// Handler depends on interfaces, not concrete types
type Handler struct {
    tmpl TemplateRenderer
    data DataLoader
}

func NewHandler(tmpl TemplateRenderer, data DataLoader) *Handler {
    return &Handler{
        tmpl: tmpl,
        data: data,
    }
}

Benefits of Interfaces

// Easy to mock for testing
type MockRenderer struct {
    RenderCalled bool
    RenderError  error
}

func (m *MockRenderer) Render(w io.Writer, name string, data interface{}) error {
    m.RenderCalled = true
    return m.RenderError
}

// Test with mock
func TestHandler(t *testing.T) {
    mock := &MockRenderer{}
    handler := NewHandler(mock, nil)

    // Test
    handler.Home(w, r)

    // Verify
    if !mock.RenderCalled {
        t.Error("expected Render to be called")
    }
}

Dependency Injection Patterns

1. Constructor Injection (Most Common in Go)

type Service struct {
    db Database
}

func NewService(db Database) *Service {
    return &Service{db: db}
}

2. Method Injection (Less Common)

type Service struct {
    // No db field
}

func (s *Service) Process(db Database, data Data) error {
    // db passed per-method call
    return db.Save(data)
}

3. Property Injection (Avoid in Go)

// Not idiomatic Go
type Service struct {
    DB Database  // Public field set after construction
}

service := &Service{}
service.DB = db  // Set dependency manually - DON'T DO THIS

Testing with Dependency Injection

Mock Dependencies

// internal/handlers/cv_pages_test.go

func TestHome(t *testing.T) {
    // Create real template manager for test
    cfg := &config.TemplateConfig{
        Dir:         "../../templates",
        PartialsDir: "../../templates/partials",
        HotReload:   true,
    }
    tmplManager, err := templates.NewManager(cfg)
    if err != nil {
        t.Fatal(err)
    }

    // Inject into handler
    handler := handlers.NewCVHandler(tmplManager, "localhost:8080")

    // Test
    req := httptest.NewRequest(http.MethodGet, "/?lang=en", nil)
    w := httptest.NewRecorder()

    handler.Home(w, req)

    // Verify
    if w.Code != http.StatusOK {
        t.Errorf("expected 200, got %d", w.Code)
    }
}

Test Doubles

// Create test double that implements interface
type StubRenderer struct {
    rendered bool
    data     interface{}
}

func (s *StubRenderer) Render(w io.Writer, name string, data interface{}) error {
    s.rendered = true
    s.data = data
    fmt.Fprintf(w, "<html>Test</html>")
    return nil
}

func TestWithStub(t *testing.T) {
    stub := &StubRenderer{}
    handler := NewHandler(stub, "test:8080")

    handler.Home(w, req)

    if !stub.rendered {
        t.Error("expected template to be rendered")
    }
}

Dependency Injection Containers

Go doesn't have built-in DI containers like some languages, but libraries exist:

Wire (Google)

// wire.go
//go:build wireinject

import "github.com/google/wire"

func InitializeHandler() (*handlers.CVHandler, error) {
    wire.Build(
        config.Load,
        templates.NewManager,
        handlers.NewCVHandler,
    )
    return &handlers.CVHandler{}, nil
}

// Wire generates code at compile time

Dig (Uber)

import "go.uber.org/dig"

func main() {
    container := dig.New()

    // Register constructors
    container.Provide(config.Load)
    container.Provide(templates.NewManager)
    container.Provide(handlers.NewCVHandler)

    // Invoke
    err := container.Invoke(func(h *handlers.CVHandler) {
        // Use handler
    })
}
// main.go - Manual wiring is clear and simple
func main() {
    cfg := config.Load()
    tmpl, _ := templates.NewManager(cfg.Templates)
    handler := handlers.NewCVHandler(tmpl, cfg.Server.Host)
    // Clear dependency graph
}

Best Practices

DO

// Accept dependencies via constructor
func NewHandler(db Database, logger Logger) *Handler {
    return &Handler{db: db, logger: logger}
}

// Depend on interfaces, not concrete types
type Handler struct {
    db Database  // Interface
}

// Make dependencies explicit
func NewService(db Database, cache Cache, queue Queue) *Service {
    // All dependencies visible in signature
}

// Validate dependencies
func NewHandler(db Database) (*Handler, error) {
    if db == nil {
        return nil, errors.New("database is required")
    }
    return &Handler{db: db}, nil
}

// Keep constructors simple
func NewHandler(tmpl *templates.Manager, host string) *Handler {
    return &Handler{tmpl: tmpl, host: host}
}

DON'T

// DON'T create dependencies inside components
func NewHandler() *Handler {
    db := connectDatabase()  // Wrong! Hard to test
    return &Handler{db: db}
}

// DON'T use global variables
var globalDB Database

func (h *Handler) Save() {
    globalDB.Save()  // Wrong! Hidden dependency
}

// DON'T make dependencies public
type Handler struct {
    DB Database  // Wrong! Should be private
}

// DON'T over-complicate with DI containers for simple apps
// Manual wiring in main() is often clearer

Circular Dependencies

Problem

// ServiceA depends on ServiceB
type ServiceA struct {
    b *ServiceB
}

// ServiceB depends on ServiceA
type ServiceB struct {
    a *ServiceA
}

// Can't construct either!

Solution: Interfaces

// Break cycle with interface
type BInterface interface {
    DoB()
}

type ServiceA struct {
    b BInterface  // Depends on interface
}

type ServiceB struct {
    // No dependency on A
}

func (b *ServiceB) DoB() {}

// Can construct
b := &ServiceB{}
a := &ServiceA{b: b}
  • Handler Pattern: Uses DI for template managers
  • Singleton Pattern: Often combined with DI
  • Factory Pattern: Can be used with DI

Further Reading