Dependency Injection in Go

Why Dependency Injection Transforms Applications

Consider building a complex machine where every component is welded together permanently. If a single part breaks, the entire machine becomes useless. If you want to upgrade one component, you must rebuild the entire machine. Now consider building with interchangeable parts—each component can be easily swapped, tested independently, and upgraded without affecting others. This is the revolutionary power of dependency injection!

Dependency injection transforms how we design and build software systems. Instead of components creating their own dependencies, they receive dependencies from external sources. This simple shift enables:

  • Testable architectures that can be validated in isolation
  • Flexible systems that can adapt to changing requirements
  • Maintainable codebases where components have clear boundaries
  • Production-ready applications that can handle different deployment environments

💡 Key Insight: Dependency injection is like building with LEGO bricks instead of welded parts—each piece can be independently tested, swapped, and upgraded without affecting the entire structure.

Real-World Transformations:

Google's Infrastructure - Scales to billions of requests:

  • Before: Manual dependency wiring across microservices
  • After: Wire code generation for compile-time DI
  • Impact: 90% reduction in initialization bugs, 10x faster onboarding

Uber's Ride-Sharing Platform - Handles millions of trips:

  • Problem: Tightly coupled services, impossible to test in isolation
  • Solution: Uber Fx framework for lifecycle management
  • Result: Clean architecture, easy testing, reliable deployments

Cloudflare's Global Network - Processes 10M+ requests per second:

  • Challenge: Complex service dependencies across edge locations
  • Approach: Custom DI container with hot-reload capabilities
  • Outcome: Zero-downtime deployments, improved developer productivity

Learning Objectives

By the end of this tutorial, you will master:

  1. DI Fundamentals

    • Understand dependency injection vs service locator patterns
    • Master constructor injection and interface-based design
    • Learn when to use DI vs when to keep it simple
  2. Manual DI Patterns

    • Build explicit dependency graphs with constructor injection
    • Implement functional options for flexible configuration
    • Design clean architectures with proper separation of concerns
  3. Advanced DI Concepts

    • Service locators and registry patterns
    • Autowiring and automatic dependency resolution
    • Dependency lifecycle management (singleton, transient, scoped)
  4. Code Generation with Wire

    • Use Google Wire for compile-time dependency injection
    • Design provider sets and injection functions
    • Build zero-runtime-overhead dependency systems
  5. Runtime DI with Dig

    • Master Uber's Dig for runtime dependency resolution
    • Build lifecycle-managed applications with Fx
    • Handle complex dependency graphs in production

Core Concepts - Understanding Dependency Injection

The Dependency Problem

Dependencies create hidden connections that make code harder to test, understand, and maintain.

 1package main
 2
 3import (
 4    "database/sql"
 5    "fmt"
 6)
 7
 8// ❌ PROBLEM: Hard-coded dependencies
 9type UserService struct {
10    // Dependencies created internally - impossible to test!
11}
12
13func NewUserService() *UserService {
14    db, err := sql.Open("postgres", "production-dsn")
15    if err != nil {
16        panic(err) // Crashes in tests!
17    }
18
19    // Hidden dependency: database connection
20    return &UserService{}
21}
22
23func (s *UserService) GetUser(id int) error {
24    // Uses hidden database connection
25    fmt.Println("Fetching user from production database")
26    return nil
27}
28
29func main() {
30    service := NewUserService()
31    service.GetUser(1) // Always uses production DB!
32}

Problems with this approach:

  • Untestable: Can't replace database with mock
  • Inflexible: Hard-coded database connection
  • Hidden Dependencies: Constructor doesn't reveal requirements
  • Production Risk: Tests might hit real database

The Dependency Injection Solution

 1package main
 2
 3import (
 4    "database/sql"
 5    "fmt"
 6)
 7
 8// ✅ SOLUTION: Injected dependencies
 9type UserService struct {
10    db *sql.DB // Explicit dependency
11}
12
13func NewUserService(db *sql.DB) *UserService {
14    return &UserService{
15        db: db, // Dependency received externally
16    }
17}
18
19func (s *UserService) GetUser(id int) error {
20    fmt.Printf("Fetching user using injected database\n")
21    return nil
22}
23
24func main() {
25    // Production usage
26    prodDB, _ := sql.Open("postgres", "production-dsn")
27    prodService := NewUserService(prodDB)
28    prodService.GetUser(1)
29
30    // Test usage
31    testDB, _ := sql.Open("sqlite3", ":memory:")
32    testService := NewUserService(testDB)
33    testService.GetUser(1) // Uses test database!
34}

Benefits of this approach:

  • Testable: Can inject mock database for tests
  • Flexible: Can use different database implementations
  • Explicit: Constructor shows all dependencies
  • Safe: Tests use isolated test database

Interface-Based Design

Interfaces provide the abstraction layer that makes DI powerful.

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7// Interface defines the contract
 8type Database interface {
 9    Query(query string, args ...interface{}) ([]interface{}, error)
10    Close() error
11}
12
13// Production implementation
14type PostgreSQLDatabase struct {
15    dsn string
16}
17
18func NewPostgreSQLDatabase(dsn string) *PostgreSQLDatabase {
19    return &PostgreSQLDatabase{dsn: dsn}
20}
21
22func (p *PostgreSQLDatabase) Query(query string, args ...interface{}) ([]interface{}, error) {
23    fmt.Printf("PostgreSQL query: %s with DSN: %s\n", query, p.dsn)
24    return []interface{}{"user1"}, nil
25}
26
27func (p *PostgreSQLDatabase) Close() error {
28    fmt.Println("Closing PostgreSQL connection")
29    return nil
30}
31
32// Test implementation
33type MockDatabase struct {
34    data []interface{}
35}
36
37func NewMockDatabase() *MockDatabase {
38    return &MockDatabase{
39        data: []interface{}{"mock_user"},
40    }
41}
42
43func (m *MockDatabase) Query(query string, args ...interface{}) ([]interface{}, error) {
44    fmt.Printf("Mock query: %s\n", query)
45    return m.data, nil
46}
47
48func (m *MockDatabase) Close() error {
49    fmt.Println("Closing mock database")
50    return nil
51}
52
53// Service depends on interface, not implementation
54type UserService struct {
55    db Database // Interface injection
56}
57
58func NewUserService(db Database) *UserService {
59    return &UserService{db: db}
60}
61
62func (s *UserService) GetUser(id int) error {
63    result, err := s.db.Query("SELECT * FROM users WHERE id = ?", id)
64    if err != nil {
65        return err
66    }
67    fmt.Printf("User found: %v\n", result)
68    return nil
69}
70
71func main() {
72    fmt.Println("=== Dependency Injection with Interfaces ===")
73
74    // Production: Use PostgreSQL
75    prodDB := NewPostgreSQLDatabase("postgres://production/db")
76    prodService := NewUserService(prodDB)
77    prodService.GetUser(1)
78
79    // Testing: Use Mock
80    mockDB := NewMockDatabase()
81    testService := NewUserService(mockDB)
82    testService.GetUser(1)
83}

Dependency Injection Patterns

Pattern 1 - Constructor Injection

The most common and explicit DI pattern in Go.

  1package main
  2
  3import (
  4    "fmt"
  5    "log"
  6    "time"
  7)
  8
  9// Define interfaces for all dependencies
 10type Logger interface {
 11    Info(msg string)
 12    Error(msg string)
 13}
 14
 15type Database interface {
 16    GetUser(id int) (map[string]interface{}, error)
 17    SaveUser(user map[string]interface{}) error
 18}
 19
 20type EmailService interface {
 21    SendEmail(to, subject, body string) error
 22}
 23
 24// Concrete implementations
 25type ConsoleLogger struct{}
 26
 27func (c *ConsoleLogger) Info(msg string) {
 28    fmt.Printf("[INFO] %s\n", msg)
 29}
 30
 31func (c *ConsoleLogger) Error(msg string) {
 32    fmt.Printf("[ERROR] %s\n", msg)
 33}
 34
 35type InMemoryDatabase struct {
 36    users map[int]map[string]interface{}
 37}
 38
 39func NewInMemoryDatabase() *InMemoryDatabase {
 40    return &InMemoryDatabase{
 41        users: make(map[int]map[string]interface{}),
 42    }
 43}
 44
 45func (db *InMemoryDatabase) GetUser(id int) (map[string]interface{}, error) {
 46    if user, exists := db.users[id]; exists {
 47        return user, nil
 48    }
 49    return nil, fmt.Errorf("user not found")
 50}
 51
 52func (db *InMemoryDatabase) SaveUser(user map[string]interface{}) error {
 53    id := user["id"].(int)
 54    db.users[id] = user
 55    return nil
 56}
 57
 58type MockEmailService struct{}
 59
 60func (m *MockEmailService) SendEmail(to, subject, body string) error {
 61    fmt.Printf("Email sent to %s: %s\n", to, subject)
 62    return nil
 63}
 64
 65// Service with all dependencies injected
 66type UserService struct {
 67    db           Database
 68    logger       Logger
 69    emailService EmailService
 70}
 71
 72func NewUserService(db Database, logger Logger, emailService EmailService) *UserService {
 73    return &UserService{
 74        db:           db,
 75        logger:       logger,
 76        emailService: emailService,
 77    }
 78}
 79
 80func (s *UserService) RegisterUser(email, name string) error {
 81    s.logger.Info("Registering new user: " + email)
 82
 83    // Check if user already exists
 84    if existingUser, err := s.db.GetUser(1); err == nil && existingUser != nil {
 85        s.logger.Info("User already exists: " + email)
 86        return fmt.Errorf("user already exists")
 87    }
 88
 89    // Create user
 90    user := map[string]interface{}{
 91        "id":         1,
 92        "email":      email,
 93        "name":       name,
 94        "created_at": time.Now(),
 95    }
 96
 97    if err := s.db.SaveUser(user); err != nil {
 98        s.logger.Error("Failed to save user: " + err.Error())
 99        return err
100    }
101
102    s.logger.Info("User registered successfully: " + email)
103
104    // Send welcome email
105    if err := s.emailService.SendEmail(email, "Welcome!", "Welcome to our service!"); err != nil {
106        s.logger.Error("Failed to send welcome email: " + err.Error())
107        // Don't fail registration if email fails
108    }
109
110    return nil
111}
112
113func main() {
114    fmt.Println("=== Constructor Injection Example ===")
115
116    // Create all dependencies
117    logger := &ConsoleLogger{}
118    database := NewInMemoryDatabase()
119    emailService := &MockEmailService{}
120
121    // Inject dependencies into service
122    userService := NewUserService(database, logger, emailService)
123
124    // Use the service
125    if err := userService.RegisterUser("john@example.com", "John Doe"); err != nil {
126        log.Printf("Registration failed: %v\n", err)
127    } else {
128        fmt.Println("Registration successful!")
129    }
130}

Pattern 2 - Setter Injection

Alternative approach using setter methods.

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7type Logger interface {
 8    Log(message string)
 9}
10
11type Database interface {
12    Query(sql string) error
13}
14
15type SimpleLogger struct{}
16
17func (s *SimpleLogger) Log(message string) {
18    fmt.Println("[LOG]", message)
19}
20
21type SimpleDatabase struct{}
22
23func (s *SimpleDatabase) Query(sql string) error {
24    fmt.Println("[DB]", sql)
25    return nil
26}
27
28// Service with setter injection
29type UserService struct {
30    logger Logger
31    db     Database
32}
33
34func NewUserService() *UserService {
35    return &UserService{}
36}
37
38// Setter methods
39func (s *UserService) SetLogger(logger Logger) {
40    s.logger = logger
41}
42
43func (s *UserService) SetDatabase(db Database) {
44    s.db = db
45}
46
47func (s *UserService) CreateUser(name string) error {
48    if s.logger == nil || s.db == nil {
49        return fmt.Errorf("dependencies not configured")
50    }
51
52    s.logger.Log("Creating user: " + name)
53    return s.db.Query("INSERT INTO users VALUES")
54}
55
56func main() {
57    fmt.Println("=== Setter Injection Example ===")
58
59    service := NewUserService()
60    service.SetLogger(&SimpleLogger{})
61    service.SetDatabase(&SimpleDatabase{})
62
63    service.CreateUser("Alice")
64}

Pattern 3 - Method Injection

Pass dependencies as method parameters.

 1package main
 2
 3import (
 4    "fmt"
 5)
 6
 7type Logger interface {
 8    Log(message string)
 9}
10
11type Database interface {
12    Query(sql string) error
13}
14
15type ConsoleLogger struct{}
16
17func (c *ConsoleLogger) Log(message string) {
18    fmt.Println("[LOG]", message)
19}
20
21type InMemoryDB struct{}
22
23func (i *InMemoryDB) Query(sql string) error {
24    fmt.Println("[DB]", sql)
25    return nil
26}
27
28// Service with method injection
29type UserService struct{}
30
31func NewUserService() *UserService {
32    return &UserService{}
33}
34
35// Dependencies passed as method parameters
36func (s *UserService) CreateUser(name string, logger Logger, db Database) error {
37    logger.Log("Creating user: " + name)
38    return db.Query("INSERT INTO users VALUES")
39}
40
41func main() {
42    fmt.Println("=== Method Injection Example ===")
43
44    service := NewUserService()
45    logger := &ConsoleLogger{}
46    db := &InMemoryDB{}
47
48    service.CreateUser("Bob", logger, db)
49}

Service Locator Pattern

Service locator provides a centralized registry for dependencies.

Basic Service Locator

 1package main
 2
 3import (
 4    "fmt"
 5    "sync"
 6)
 7
 8type ServiceLocator struct {
 9    mu       sync.RWMutex
10    services map[string]interface{}
11}
12
13func NewServiceLocator() *ServiceLocator {
14    return &ServiceLocator{
15        services: make(map[string]interface{}),
16    }
17}
18
19func (sl *ServiceLocator) Register(name string, service interface{}) {
20    sl.mu.Lock()
21    defer sl.mu.Unlock()
22    sl.services[name] = service
23}
24
25func (sl *ServiceLocator) Get(name string) (interface{}, bool) {
26    sl.mu.RLock()
27    defer sl.mu.RUnlock()
28    service, exists := sl.services[name]
29    return service, exists
30}
31
32// Example services
33type Logger interface {
34    Log(message string)
35}
36
37type ConsoleLogger struct{}
38
39func (c *ConsoleLogger) Log(message string) {
40    fmt.Println("[LOG]", message)
41}
42
43type Database interface {
44    Query(sql string) error
45}
46
47type MockDatabase struct{}
48
49func (m *MockDatabase) Query(sql string) error {
50    fmt.Println("[DB]", sql)
51    return nil
52}
53
54// Service using locator
55type UserService struct {
56    locator *ServiceLocator
57}
58
59func NewUserService(locator *ServiceLocator) *UserService {
60    return &UserService{locator: locator}
61}
62
63func (s *UserService) CreateUser(name string) error {
64    // Retrieve dependencies from locator
65    loggerInterface, ok := s.locator.Get("logger")
66    if !ok {
67        return fmt.Errorf("logger not found")
68    }
69    logger := loggerInterface.(Logger)
70
71    dbInterface, ok := s.locator.Get("database")
72    if !ok {
73        return fmt.Errorf("database not found")
74    }
75    db := dbInterface.(Database)
76
77    logger.Log("Creating user: " + name)
78    return db.Query("INSERT INTO users VALUES")
79}
80
81func main() {
82    fmt.Println("=== Service Locator Pattern ===")
83
84    // Setup service locator
85    locator := NewServiceLocator()
86    locator.Register("logger", &ConsoleLogger{})
87    locator.Register("database", &MockDatabase{})
88
89    // Create service
90    service := NewUserService(locator)
91    service.CreateUser("Charlie")
92}

Type-Safe Service Locator

 1package main
 2
 3import (
 4    "fmt"
 5    "reflect"
 6    "sync"
 7)
 8
 9type TypedServiceLocator struct {
10    mu       sync.RWMutex
11    services map[reflect.Type]interface{}
12}
13
14func NewTypedServiceLocator() *TypedServiceLocator {
15    return &TypedServiceLocator{
16        services: make(map[reflect.Type]interface{}),
17    }
18}
19
20func (sl *TypedServiceLocator) Register(service interface{}) {
21    sl.mu.Lock()
22    defer sl.mu.Unlock()
23
24    t := reflect.TypeOf(service)
25    sl.services[t] = service
26}
27
28func (sl *TypedServiceLocator) Get(serviceType interface{}) (interface{}, bool) {
29    sl.mu.RLock()
30    defer sl.mu.RUnlock()
31
32    t := reflect.TypeOf(serviceType).Elem()
33    service, exists := sl.services[t]
34    return service, exists
35}
36
37// Example usage
38type Logger interface {
39    Log(message string)
40}
41
42type FileLogger struct{}
43
44func (f *FileLogger) Log(message string) {
45    fmt.Println("[FILE]", message)
46}
47
48func main() {
49    fmt.Println("=== Type-Safe Service Locator ===")
50
51    locator := NewTypedServiceLocator()
52
53    // Register service
54    logger := &FileLogger{}
55    locator.Register(logger)
56
57    // Retrieve service by type
58    var l Logger
59    retrieved, ok := locator.Get(&l)
60    if ok {
61        logger := retrieved.(*FileLogger)
62        logger.Log("Type-safe retrieval")
63    }
64}

Autowiring and Automatic Dependency Resolution

Reflection-Based Autowiring

  1package main
  2
  3import (
  4    "fmt"
  5    "reflect"
  6)
  7
  8type Container struct {
  9    services map[reflect.Type]interface{}
 10}
 11
 12func NewContainer() *Container {
 13    return &Container{
 14        services: make(map[reflect.Type]interface{}),
 15    }
 16}
 17
 18func (c *Container) Register(service interface{}) {
 19    t := reflect.TypeOf(service)
 20    c.services[t] = service
 21}
 22
 23func (c *Container) Resolve(target interface{}) error {
 24    targetValue := reflect.ValueOf(target)
 25    if targetValue.Kind() != reflect.Ptr {
 26        return fmt.Errorf("target must be a pointer")
 27    }
 28
 29    targetValue = targetValue.Elem()
 30    targetType := targetValue.Type()
 31
 32    if targetType.Kind() != reflect.Struct {
 33        return fmt.Errorf("target must be a struct pointer")
 34    }
 35
 36    // Iterate through struct fields
 37    for i := 0; i < targetValue.NumField(); i++ {
 38        field := targetValue.Field(i)
 39        fieldType := targetType.Field(i)
 40
 41        // Check for inject tag
 42        if tag := fieldType.Tag.Get("inject"); tag != "" {
 43            // Find matching service
 44            service, ok := c.services[field.Type()]
 45            if !ok {
 46                return fmt.Errorf("service not found for type: %v", field.Type())
 47            }
 48
 49            // Set the field
 50            field.Set(reflect.ValueOf(service))
 51        }
 52    }
 53
 54    return nil
 55}
 56
 57// Example services
 58type Logger interface {
 59    Log(message string)
 60}
 61
 62type ConsoleLogger struct{}
 63
 64func (c *ConsoleLogger) Log(message string) {
 65    fmt.Println("[LOG]", message)
 66}
 67
 68type Database interface {
 69    Query(sql string) error
 70}
 71
 72type MockDB struct{}
 73
 74func (m *MockDB) Query(sql string) error {
 75    fmt.Println("[DB]", sql)
 76    return nil
 77}
 78
 79// Service with autowired dependencies
 80type UserService struct {
 81    Logger   Logger   `inject:""`
 82    Database Database `inject:""`
 83}
 84
 85func (s *UserService) CreateUser(name string) error {
 86    s.Logger.Log("Creating user: " + name)
 87    return s.Database.Query("INSERT INTO users VALUES")
 88}
 89
 90func main() {
 91    fmt.Println("=== Autowiring Example ===")
 92
 93    container := NewContainer()
 94
 95    // Register dependencies
 96    var logger Logger = &ConsoleLogger{}
 97    var db Database = &MockDB{}
 98    container.Register(logger)
 99    container.Register(db)
100
101    // Create service with autowiring
102    service := &UserService{}
103    if err := container.Resolve(service); err != nil {
104        fmt.Printf("Autowiring failed: %v\n", err)
105        return
106    }
107
108    service.CreateUser("Dave")
109}

Practical Examples - From Concepts to Code

Example 1 - Multi-Layer Architecture

Building proper layered architecture with DI.

  1package main
  2
  3import (
  4    "fmt"
  5    "strconv"
  6)
  7
  8// ==== Domain Layer ====
  9
 10type User struct {
 11    ID    int    `json:"id"`
 12    Name  string `json:"name"`
 13    Email string `json:"email"`
 14}
 15
 16type UserRepository interface {
 17    FindByID(id int) (*User, error)
 18    Save(user *User) error
 19}
 20
 21type OrderRepository interface {
 22    FindByUserID(userID int) ([]map[string]interface{}, error)
 23    Save(order map[string]interface{}) error
 24}
 25
 26// ==== Service Layer ====
 27
 28type UserService struct {
 29    userRepo  UserRepository
 30    orderRepo OrderRepository
 31}
 32
 33func NewUserService(userRepo UserRepository, orderRepo OrderRepository) *UserService {
 34    return &UserService{
 35        userRepo:  userRepo,
 36        orderRepo: orderRepo,
 37    }
 38}
 39
 40type UserProfile struct {
 41    User   User
 42    Orders []map[string]interface{}
 43}
 44
 45func (s *UserService) GetUserProfile(userID int) (*UserProfile, error) {
 46    // Get user from repository
 47    user, err := s.userRepo.FindByID(userID)
 48    if err != nil {
 49        return nil, err
 50    }
 51
 52    // Get user's orders
 53    orders, err := s.orderRepo.FindByUserID(userID)
 54    if err != nil {
 55        return nil, err
 56    }
 57
 58    return &UserProfile{
 59        User:   *user,
 60        Orders: orders,
 61    }, nil
 62}
 63
 64// ==== Repository Layer ====
 65
 66type InMemoryUserRepository struct {
 67    users map[int]*User
 68}
 69
 70func NewInMemoryUserRepository() *InMemoryUserRepository {
 71    return &InMemoryUserRepository{
 72        users: make(map[int]*User),
 73    }
 74}
 75
 76func (r *InMemoryUserRepository) FindByID(id int) (*User, error) {
 77    if user, exists := r.users[id]; exists {
 78        return user, nil
 79    }
 80    return nil, fmt.Errorf("user not found")
 81}
 82
 83func (r *InMemoryUserRepository) Save(user *User) error {
 84    if r.users == nil {
 85        r.users = make(map[int]*User)
 86    }
 87    r.users[user.ID] = user
 88    return nil
 89}
 90
 91type InMemoryOrderRepository struct {
 92    orders []map[string]interface{}
 93}
 94
 95func NewInMemoryOrderRepository() *InMemoryOrderRepository {
 96    return &InMemoryOrderRepository{
 97        orders: make([]map[string]interface{}, 0),
 98    }
 99}
100
101func (r *InMemoryOrderRepository) FindByUserID(userID int) ([]map[string]interface{}, error) {
102    var userOrders []map[string]interface{}
103    for _, order := range r.orders {
104        if order["user_id"] == userID {
105            userOrders = append(userOrders, order)
106        }
107    }
108    return userOrders, nil
109}
110
111func (r *InMemoryOrderRepository) Save(order map[string]interface{}) error {
112    r.orders = append(r.orders, order)
113    return nil
114}
115
116// ==== Presentation Layer ====
117
118type UserHandler struct {
119    userService *UserService
120}
121
122func NewUserHandler(userService *UserService) *UserHandler {
123    return &UserHandler{
124        userService: userService,
125    }
126}
127
128func (h *UserHandler) HandleGetUser(userID string) (*UserProfile, error) {
129    id, err := strconv.Atoi(userID)
130    if err != nil {
131        return nil, fmt.Errorf("invalid user ID")
132    }
133
134    return h.userService.GetUserProfile(id)
135}
136
137func main() {
138    fmt.Println("=== Multi-Layer Architecture with DI ===")
139
140    // Create repositories
141    userRepo := NewInMemoryUserRepository()
142    orderRepo := NewInMemoryOrderRepository()
143
144    // Create service
145    userService := NewUserService(userRepo, orderRepo)
146
147    // Create handler
148    userHandler := NewUserHandler(userService)
149
150    // Seed some data
151    userRepo.Save(&User{ID: 1, Name: "John Doe", Email: "john@example.com"})
152    orderRepo.Save(map[string]interface{}{
153        "id":      1,
154        "user_id": 1,
155        "total":   99.99,
156        "status":  "completed",
157    })
158
159    // Use the handler
160    profile, err := userHandler.HandleGetUser("1")
161    if err != nil {
162        fmt.Printf("Error: %v\n", err)
163        return
164    }
165
166    fmt.Printf("User Profile:\n")
167    fmt.Printf("  Name: %s\n", profile.User.Name)
168    fmt.Printf("  Email: %s\n", profile.User.Email)
169    fmt.Printf("  Orders: %d\n", len(profile.Orders))
170}

Example 2 - Functional Options Pattern

For flexible configuration with optional parameters.

  1package main
  2
  3import (
  4    "fmt"
  5    "time"
  6)
  7
  8type ServerConfig struct {
  9    Host           string
 10    Port           int
 11    Timeout        time.Duration
 12    EnableLogging  bool
 13    Logger         Logger
 14    DatabaseURL    string
 15    MaxConnections int
 16}
 17
 18// Option type for functional configuration
 19type ServerOption func(*ServerConfig)
 20
 21// Option constructors
 22func WithHost(host string) ServerOption {
 23    return func(config *ServerConfig) {
 24        config.Host = host
 25    }
 26}
 27
 28func WithPort(port int) ServerOption {
 29    return func(config *ServerConfig) {
 30        config.Port = port
 31    }
 32}
 33
 34func WithTimeout(timeout time.Duration) ServerOption {
 35    return func(config *ServerConfig) {
 36        config.Timeout = timeout
 37    }
 38}
 39
 40func WithLogging(enable bool) ServerOption {
 41    return func(config *ServerConfig) {
 42        config.EnableLogging = enable
 43    }
 44}
 45
 46func WithLogger(logger Logger) ServerOption {
 47    return func(config *ServerConfig) {
 48        config.Logger = logger
 49    }
 50}
 51
 52func WithDatabaseURL(url string) ServerOption {
 53    return func(config *ServerConfig) {
 54        config.DatabaseURL = url
 55    }
 56}
 57
 58func WithMaxConnections(max int) ServerOption {
 59    return func(config *ServerConfig) {
 60        config.MaxConnections = max
 61    }
 62}
 63
 64type Logger interface {
 65    Log(message string)
 66}
 67
 68type ConsoleLogger struct{}
 69
 70func (c *ConsoleLogger) Log(message string) {
 71    fmt.Printf("[LOG] %s\n", message)
 72}
 73
 74type Server struct {
 75    config ServerConfig
 76}
 77
 78// Constructor accepts optional configuration
 79func NewServer(opts ...ServerOption) *Server {
 80    // Set defaults
 81    config := ServerConfig{
 82        Host:           "localhost",
 83        Port:           8080,
 84        Timeout:        30 * time.Second,
 85        EnableLogging:  true,
 86        Logger:         &ConsoleLogger{},
 87        DatabaseURL:    "sqlite3://app.db",
 88        MaxConnections: 100,
 89    }
 90
 91    // Apply provided options
 92    for _, opt := range opts {
 93        opt(&config)
 94    }
 95
 96    // Validate configuration
 97    if config.Port < 1 || config.Port > 65535 {
 98        panic("invalid port number")
 99    }
100    if config.Timeout <= 0 {
101        panic("timeout must be positive")
102    }
103
104    return &Server{config: config}
105}
106
107func (s *Server) Start() error {
108    fmt.Printf("Starting server on %s:%d\n", s.config.Host, s.config.Port)
109    fmt.Printf("Timeout: %v\n", s.config.Timeout)
110    fmt.Printf("Logging enabled: %t\n", s.config.EnableLogging)
111    fmt.Printf("Database: %s\n", s.config.DatabaseURL)
112    fmt.Printf("Max connections: %d\n", s.config.MaxConnections)
113
114    if s.config.EnableLogging {
115        s.config.Logger.Log("Server started successfully")
116    }
117
118    // Simulate server startup
119    return nil
120}
121
122func main() {
123    fmt.Println("=== Functional Options Pattern ===")
124
125    // Default server
126    defaultServer := NewServer()
127    fmt.Println("\n--- Default Server ---")
128    defaultServer.Start()
129
130    // Custom server with options
131    customServer := NewServer(
132        WithHost("0.0.0.0"),
133        WithPort(9090),
134        WithTimeout(60*time.Second),
135        WithDatabaseURL("postgres://user:pass@localhost/db"),
136        WithMaxConnections(1000),
137    )
138    fmt.Println("\n--- Custom Server ---")
139    customServer.Start()
140
141    // Server with disabled logging
142    silentServer := NewServer(
143        WithPort(3000),
144        WithLogging(false),
145    )
146    fmt.Println("\n--- Silent Server ---")
147    silentServer.Start()
148}

Example 3 - Dependency Lifecycle Management

Managing singleton, transient, and scoped lifetimes.

  1package main
  2
  3import (
  4    "fmt"
  5    "reflect"
  6    "sync"
  7    "time"
  8)
  9
 10type Lifetime int
 11
 12const (
 13    Transient Lifetime = iota
 14    Singleton
 15    Scoped
 16)
 17
 18type ServiceRegistration struct {
 19    Factory  interface{}
 20    Lifetime Lifetime
 21}
 22
 23type Container struct {
 24    mu            sync.RWMutex
 25    registrations map[reflect.Type]ServiceRegistration
 26    singletons    map[reflect.Type]interface{}
 27}
 28
 29func NewContainer() *Container {
 30    return &Container{
 31        registrations: make(map[reflect.Type]ServiceRegistration),
 32        singletons:    make(map[reflect.Type]interface{}),
 33    }
 34}
 35
 36func (c *Container) RegisterTransient(factory interface{}) {
 37    c.mu.Lock()
 38    defer c.mu.Unlock()
 39
 40    factoryType := reflect.TypeOf(factory)
 41    serviceType := factoryType.Out(0)
 42
 43    c.registrations[serviceType] = ServiceRegistration{
 44        Factory:  factory,
 45        Lifetime: Transient,
 46    }
 47}
 48
 49func (c *Container) RegisterSingleton(factory interface{}) {
 50    c.mu.Lock()
 51    defer c.mu.Unlock()
 52
 53    factoryType := reflect.TypeOf(factory)
 54    serviceType := factoryType.Out(0)
 55
 56    c.registrations[serviceType] = ServiceRegistration{
 57        Factory:  factory,
 58        Lifetime: Singleton,
 59    }
 60}
 61
 62func (c *Container) Resolve(serviceType reflect.Type) (interface{}, error) {
 63    c.mu.RLock()
 64    registration, exists := c.registrations[serviceType]
 65    c.mu.RUnlock()
 66
 67    if !exists {
 68        return nil, fmt.Errorf("service not registered: %v", serviceType)
 69    }
 70
 71    switch registration.Lifetime {
 72    case Singleton:
 73        // Check if singleton already created
 74        c.mu.RLock()
 75        if singleton, ok := c.singletons[serviceType]; ok {
 76            c.mu.RUnlock()
 77            return singleton, nil
 78        }
 79        c.mu.RUnlock()
 80
 81        // Create singleton
 82        instance := c.createInstance(registration.Factory)
 83
 84        c.mu.Lock()
 85        c.singletons[serviceType] = instance
 86        c.mu.Unlock()
 87
 88        return instance, nil
 89
 90    case Transient:
 91        // Create new instance each time
 92        return c.createInstance(registration.Factory), nil
 93
 94    default:
 95        return nil, fmt.Errorf("unsupported lifetime: %v", registration.Lifetime)
 96    }
 97}
 98
 99func (c *Container) createInstance(factory interface{}) interface{} {
100    factoryValue := reflect.ValueOf(factory)
101    results := factoryValue.Call(nil)
102    return results[0].Interface()
103}
104
105// Example services
106type Logger interface {
107    Log(message string)
108}
109
110type RequestLogger struct {
111    id int
112}
113
114func (r *RequestLogger) Log(message string) {
115    fmt.Printf("[Logger-%d] %s\n", r.id, message)
116}
117
118var loggerID int
119
120func NewRequestLogger() Logger {
121    loggerID++
122    return &RequestLogger{id: loggerID}
123}
124
125type Database interface {
126    Query(sql string) error
127}
128
129type DBConnection struct {
130    id int
131}
132
133func (d *DBConnection) Query(sql string) error {
134    fmt.Printf("[DB-%d] %s\n", d.id, sql)
135    return nil
136}
137
138var dbID int
139
140func NewDBConnection() Database {
141    dbID++
142    return &DBConnection{id: dbID}
143}
144
145func main() {
146    fmt.Println("=== Dependency Lifecycle Management ===")
147
148    container := NewContainer()
149
150    // Register logger as transient (new instance each time)
151    container.RegisterTransient(NewRequestLogger)
152
153    // Register database as singleton (same instance)
154    container.RegisterSingleton(NewDBConnection)
155
156    // Resolve logger multiple times (different instances)
157    var logger1, logger2 Logger
158    l1, _ := container.Resolve(reflect.TypeOf((*Logger)(nil)).Elem())
159    l2, _ := container.Resolve(reflect.TypeOf((*Logger)(nil)).Elem())
160    logger1 = l1.(Logger)
161    logger2 = l2.(Logger)
162
163    logger1.Log("First logger")
164    logger2.Log("Second logger")
165
166    // Resolve database multiple times (same instance)
167    var db1, db2 Database
168    d1, _ := container.Resolve(reflect.TypeOf((*Database)(nil)).Elem())
169    d2, _ := container.Resolve(reflect.TypeOf((*Database)(nil)).Elem())
170    db1 = d1.(Database)
171    db2 = d2.(Database)
172
173    db1.Query("SELECT * FROM users")
174    db2.Query("SELECT * FROM orders")
175
176    fmt.Printf("\nSame database instance? %t\n", d1 == d2)
177    fmt.Printf("Same logger instance? %t\n", l1 == l2)
178}

Common Patterns and Production Pitfalls

Pattern 1 - Interface Segregation

Keep interfaces small and focused.

  1package main
  2
  3import (
  4    "fmt"
  5)
  6
  7// ❌ BAD: Large interface
  8type UserRepository interface {
  9    Create(user map[string]interface{}) error
 10    FindByID(id int) (map[string]interface{}, error)
 11    FindByEmail(email string) (map[string]interface{}, error)
 12    Update(user map[string]interface{}) error
 13    Delete(id int) error
 14    List(offset, limit int) ([]map[string]interface{}, error)
 15    Search(query string) ([]map[string]interface{}, error)
 16    UpdatePassword(userID int, password string) error
 17    UpdateProfile(userID int, profile map[string]interface{}) error
 18    GetStats(userID int) (map[string]interface{}, error)
 19}
 20
 21// ✅ GOOD: Small, focused interfaces
 22type UserReader interface {
 23    FindByID(id int) (map[string]interface{}, error)
 24    FindByEmail(email string) (map[string]interface{}, error)
 25}
 26
 27type UserWriter interface {
 28    Create(user map[string]interface{}) error
 29    Update(user map[string]interface{}) error
 30    Delete(id int) error
 31}
 32
 33type UserSearcher interface {
 34    Search(query string) ([]map[string]interface{}, error)
 35    List(offset, limit int) ([]map[string]interface{}, error)
 36}
 37
 38// Service depends only on what it needs
 39type UserAuthService struct {
 40    reader UserReader // Only needs to read users
 41}
 42
 43func NewUserAuthService(reader UserReader) *UserAuthService {
 44    return &UserAuthService{reader: reader}
 45}
 46
 47func (s *UserAuthService) Authenticate(email string, password string) bool {
 48    user, err := s.reader.FindByEmail(email)
 49    if err != nil {
 50        return false
 51    }
 52    // Authenticate user...
 53    _ = user
 54    return true
 55}
 56
 57func main() {
 58    fmt.Println("=== Interface Segregation ===")
 59
 60    // Implementation that implements all small interfaces
 61    type FullUserRepository struct{}
 62
 63    func (r *FullUserRepository) FindByID(id int) (map[string]interface{}, error) {
 64        return map[string]interface{}{"id": id}, nil
 65    }
 66
 67    func (r *FullUserRepository) FindByEmail(email string) (map[string]interface{}, error) {
 68        return map[string]interface{}{"email": email}, nil
 69    }
 70
 71    func (r *FullUserRepository) Create(user map[string]interface{}) error {
 72        fmt.Println("Creating user:", user["email"])
 73        return nil
 74    }
 75
 76    func (r *FullUserRepository) Update(user map[string]interface{}) error {
 77        fmt.Println("Updating user:", user["id"])
 78        return nil
 79    }
 80
 81    func (r *FullUserRepository) Delete(id int) error {
 82        fmt.Println("Deleting user:", id)
 83        return nil
 84    }
 85
 86    func (r *FullUserRepository) Search(query string) ([]map[string]interface{}, error) {
 87        return []map[string]interface{}{}, nil
 88    }
 89
 90    func (r *FullUserRepository) List(offset, limit int) ([]map[string]interface{}, error) {
 91        return []map[string]interface{}{}, nil
 92    }
 93
 94    // Service only needs reader interface
 95    repo := &FullUserRepository{}
 96    authService := NewUserAuthService(repo)
 97
 98    if authService.Authenticate("test@example.com", "password") {
 99        fmt.Println("Authentication successful")
100    } else {
101        fmt.Println("Authentication failed")
102    }
103}

Pattern 2 - Dependency Graph Visualization

Understanding and documenting dependency relationships.

  1package main
  2
  3import (
  4    "fmt"
  5    "strings"
  6)
  7
  8// Service definitions for dependency graph
  9type ServiceType string
 10
 11const (
 12    ServiceLogger   ServiceType = "Logger"
 13    ServiceDatabase ServiceType = "Database"
 14    ServiceCache    ServiceType = "Cache"
 15    ServiceEmail    ServiceType = "Email"
 16    ServiceUser     ServiceType = "User"
 17    ServiceAuth     ServiceType = "Auth"
 18    ServicePayment  ServiceType = "Payment"
 19    ServiceAPI      ServiceType = "API"
 20)
 21
 22type ServiceDependency struct {
 23    Service   ServiceType
 24    DependsOn []ServiceType
 25}
 26
 27// Define dependency graph
 28var dependencyGraph = []ServiceDependency{
 29    {Service: ServiceLogger, DependsOn: []ServiceType{}},
 30    {Service: ServiceDatabase, DependsOn: []ServiceType{ServiceLogger}},
 31    {Service: ServiceCache, DependsOn: []ServiceType{ServiceLogger}},
 32    {Service: ServiceEmail, DependsOn: []ServiceType{ServiceLogger}},
 33    {Service: ServiceUser, DependsOn: []ServiceType{ServiceDatabase, ServiceCache, ServiceLogger}},
 34    {Service: ServiceAuth, DependsOn: []ServiceType{ServiceUser, ServiceLogger}},
 35    {Service: ServicePayment, DependsOn: []ServiceType{ServiceUser, ServiceLogger}},
 36    {Service: ServiceAPI, DependsOn: []ServiceType{ServiceUser, ServiceAuth, ServicePayment, ServiceLogger}},
 37}
 38
 39// Service registry for DI container
 40type ServiceRegistry struct {
 41    services map[ServiceType]interface{}
 42}
 43
 44func NewServiceRegistry() *ServiceRegistry {
 45    return &ServiceRegistry{
 46        services: make(map[ServiceType]interface{}),
 47    }
 48}
 49
 50func (r *ServiceRegistry) Register(serviceType ServiceType, service interface{}) {
 51    r.services[serviceType] = service
 52}
 53
 54func (r *ServiceRegistry) Get(serviceType ServiceType) interface{} {
 55    return r.services[serviceType]
 56}
 57
 58func (r *ServiceRegistry) InitializeDependencies() error {
 59    // Initialize in dependency order
 60    for _, dep := range dependencyGraph {
 61        fmt.Printf("Initializing %s...\n", dep.Service)
 62
 63        // Check if all dependencies are available
 64        for _, depOn := range dep.DependsOn {
 65            if _, exists := r.services[depOn]; !exists {
 66                return fmt.Errorf("missing dependency: %s required by %s", depOn, dep.Service)
 67            }
 68        }
 69
 70        fmt.Printf("  Dependencies: %v\n", dep.DependsOn)
 71        fmt.Printf("  ✓ Initialized\n")
 72    }
 73
 74    return nil
 75}
 76
 77func (r *ServiceRegistry) PrintDependencyGraph() {
 78    fmt.Println("=== Dependency Graph ===")
 79    for _, dep := range dependencyGraph {
 80        if len(dep.DependsOn) == 0 {
 81            fmt.Printf("%s\n", dep.Service)
 82        } else {
 83            fmt.Printf("%s depends on: %s\n", dep.Service, strings.Join(convertToStrings(dep.DependsOn), ", "))
 84        }
 85    }
 86}
 87
 88func convertToStrings(types []ServiceType) []string {
 89    result := make([]string, len(types))
 90    for i, t := range types {
 91        result[i] = string(t)
 92    }
 93    return result
 94}
 95
 96// Mock implementations
 97type MockService struct {
 98    name string
 99}
100
101func NewMockService(name string) *MockService {
102    return &MockService{name: name}
103}
104
105func main() {
106    fmt.Println("=== Dependency Injection Container ===")
107
108    registry := NewServiceRegistry()
109
110    // Register all services
111    registry.Register(ServiceLogger, NewMockService("ConsoleLogger"))
112    registry.Register(ServiceDatabase, NewMockService("PostgresDatabase"))
113    registry.Register(ServiceCache, NewMockService("RedisCache"))
114    registry.Register(ServiceEmail, NewMockService("SMTPEmail"))
115    registry.Register(ServiceUser, NewMockService("UserService"))
116    registry.Register(ServiceAuth, NewMockService("JWTAuth"))
117    registry.Register(ServicePayment, NewMockService("StripePayment"))
118    registry.Register(ServiceAPI, NewMockService("RestAPI"))
119
120    // Print dependency graph
121    registry.PrintDependencyGraph()
122
123    // Initialize in correct order
124    fmt.Println("\n=== Service Initialization ===")
125    if err := registry.InitializeDependencies(); err != nil {
126        fmt.Printf("Initialization failed: %v\n", err)
127        return
128    }
129
130    fmt.Println("\n✅ All services initialized successfully!")
131
132    // Example usage
133    fmt.Println("\n=== Service Usage ===")
134    userService := registry.Get(ServiceUser)
135    fmt.Printf("User service: %v\n", userService)
136}

Pattern 3 - Circular Dependency Detection

Prevent and handle circular dependencies in DI systems.

  1package main
  2
  3import (
  4    "fmt"
  5)
  6
  7// Circular dependency detection
  8type CircularDependencyError struct {
  9    Cycle []string
 10}
 11
 12func (e *CircularDependencyError) Error() string {
 13    return fmt.Sprintf("circular dependency detected: %v", e.Cycle)
 14}
 15
 16type DependencyNode struct {
 17    Service   string
 18    DependsOn []string
 19}
 20
 21type DependencyGraph struct {
 22    nodes map[string]*DependencyNode
 23}
 24
 25func NewDependencyGraph() *DependencyGraph {
 26    return &DependencyGraph{
 27        nodes: make(map[string]*DependencyNode),
 28    }
 29}
 30
 31func (g *DependencyGraph) AddService(service string, dependsOn []string) {
 32    g.nodes[service] = &DependencyNode{
 33        Service:   service,
 34        DependsOn: dependsOn,
 35    }
 36}
 37
 38func (g *DependencyGraph) CheckCircularDependencies() error {
 39    visited := make(map[string]bool)
 40    recStack := make(map[string]bool)
 41    path := []string{}
 42
 43    for service := range g.nodes {
 44        if !visited[service] {
 45            if err := g.dfsCheck(service, visited, recStack, &path); err != nil {
 46                return err
 47            }
 48        }
 49    }
 50
 51    return nil
 52}
 53
 54func (g *DependencyGraph) dfsCheck(service string, visited, recStack map[string]bool, path *[]string) error {
 55    visited[service] = true
 56    recStack[service] = true
 57    *path = append(*path, service)
 58
 59    if node, exists := g.nodes[service]; exists {
 60        for _, dep := range node.DependsOn {
 61            if !visited[dep] {
 62                if err := g.dfsCheck(dep, visited, recStack, path); err != nil {
 63                    return err
 64                }
 65            } else if recStack[dep] {
 66                // Circular dependency found
 67                cycleStart := -1
 68                for i, svc := range *path {
 69                    if svc == dep {
 70                        cycleStart = i
 71                        break
 72                    }
 73                }
 74
 75                cycle := (*path)[cycleStart:]
 76                cycle = append(cycle, dep) // Complete the cycle
 77
 78                return &CircularDependencyError{Cycle: cycle}
 79            }
 80        }
 81    }
 82
 83    recStack[service] = false
 84    *path = (*path)[:len(*path)-1] // Remove current node from path
 85    return nil
 86}
 87
 88func (g *DependencyGraph) PrintGraph() {
 89    fmt.Println("=== Dependency Graph ===")
 90    for service, node := range g.nodes {
 91        if len(node.DependsOn) == 0 {
 92            fmt.Printf("%s\n", service)
 93        } else {
 94            fmt.Printf("%s depends on: %v\n", service, node.DependsOn)
 95        }
 96    }
 97}
 98
 99func main() {
100    fmt.Println("=== Circular Dependency Detection ===")
101
102    // Create dependency graph
103    graph := NewDependencyGraph()
104
105    // Add services
106    graph.AddService("Logger", []string{})
107    graph.AddService("Database", []string{"Logger"})
108    graph.AddService("Cache", []string{"Logger"})
109    graph.AddService("UserService", []string{"Database", "Cache"})
110    graph.AddService("AuthService", []string{"UserService", "Logger"})
111
112    // Add circular dependency for demonstration
113    graph.AddService("CircularA", []string{"CircularB"})
114    graph.AddService("CircularB", []string{"CircularC"})
115    graph.AddService("CircularC", []string{"CircularA"}) // Creates cycle
116
117    graph.PrintGraph()
118
119    // Check for circular dependencies
120    fmt.Println("\n=== Checking Circular Dependencies ===")
121    if err := graph.CheckCircularDependencies(); err != nil {
122        fmt.Printf("❌ Circular dependency detected: %v\n", err)
123    } else {
124        fmt.Println("✅ No circular dependencies found")
125    }
126}

Integration and Mastery - Production Systems

Master Class - Build a DI Container from Scratch

  1package main
  2
  3import (
  4    "fmt"
  5    "reflect"
  6    "sync"
  7)
  8
  9// DI Container with reflection-based resolution
 10type DIContainer struct {
 11    mu          sync.RWMutex
 12    services    map[reflect.Type]interface{}
 13    factories   map[reflect.Type]func(*DIContainer) interface{}
 14    singletons  map[reflect.Type]interface{}
 15    options     ContainerOptions
 16}
 17
 18type ContainerOptions struct {
 19    AutoWire   bool
 20    StrictMode bool
 21    DebugMode  bool
 22}
 23
 24func NewDIContainer(options ContainerOptions) *DIContainer {
 25    return &DIContainer{
 26        services:   make(map[reflect.Type]interface{}),
 27        factories:  make(map[reflect.Type]func(*DIContainer) interface{}),
 28        singletons: make(map[reflect.Type]interface{}),
 29        options:    options,
 30    }
 31}
 32
 33// Register concrete type
 34func (c *DIContainer) Register(service interface{}) {
 35    c.mu.Lock()
 36    defer c.mu.Unlock()
 37
 38    t := reflect.TypeOf(service)
 39    c.services[t] = service
 40
 41    if c.options.DebugMode {
 42        fmt.Printf("Registered: %T\n", service)
 43    }
 44}
 45
 46// Register factory function
 47func (c *DIContainer) RegisterFactory(factory interface{}) {
 48    c.mu.Lock()
 49    defer c.mu.Unlock()
 50
 51    factoryType := reflect.TypeOf(factory)
 52    if factoryType.Kind() != reflect.Func {
 53        panic("factory must be a function")
 54    }
 55
 56    // Extract return type
 57    returnType := factoryType.Out(0)
 58    c.factories[returnType] = func(container *DIContainer) interface{} {
 59        factoryValue := reflect.ValueOf(factory)
 60
 61        // Resolve dependencies
 62        args := make([]reflect.Value, factoryType.NumIn())
 63        for i := 0; i < factoryType.NumIn(); i++ {
 64            argType := factoryType.In(i)
 65            dep, err := container.resolveType(argType)
 66            if err != nil {
 67                panic(fmt.Sprintf("failed to resolve dependency %s: %v", argType, err))
 68            }
 69            args[i] = reflect.ValueOf(dep)
 70        }
 71
 72        // Call factory with resolved dependencies
 73        results := factoryValue.Call(args)
 74        return results[0].Interface()
 75    }
 76
 77    if c.options.DebugMode {
 78        fmt.Printf("Registered factory: %T\n", factory)
 79    }
 80}
 81
 82// Register singleton
 83func (c *DIContainer) RegisterSingleton(service interface{}) {
 84    c.mu.Lock()
 85    defer c.mu.Unlock()
 86
 87    t := reflect.TypeOf(service)
 88    c.singletons[t] = service
 89
 90    if c.options.DebugMode {
 91        fmt.Printf("Registered singleton: %T\n", service)
 92    }
 93}
 94
 95// Resolve service by type
 96func (c *DIContainer) Resolve(serviceType reflect.Type) (interface{}, error) {
 97    c.mu.RLock()
 98
 99    // Check singletons first
100    if singleton, exists := c.singletons[serviceType]; exists {
101        c.mu.RUnlock()
102        return singleton, nil
103    }
104
105    // Check registered services
106    if service, exists := c.services[serviceType]; exists {
107        c.mu.RUnlock()
108        return service, nil
109    }
110    c.mu.RUnlock()
111
112    // Try to create using factory
113    if factory, exists := c.factories[serviceType]; exists {
114        return factory(c), nil
115    }
116
117    // Auto-wire if enabled
118    if c.options.AutoWire {
119        return c.autoWire(serviceType)
120    }
121
122    if c.options.StrictMode {
123        return nil, fmt.Errorf("service not registered: %v", serviceType)
124    }
125
126    return nil, nil
127}
128
129// Resolve service by value
130func (c *DIContainer) ResolveByValue(service interface{}) (interface{}, error) {
131    serviceType := reflect.TypeOf(service)
132    if serviceType.Kind() == reflect.Ptr {
133        serviceType = serviceType.Elem()
134    }
135    return c.Resolve(serviceType)
136}
137
138func (c *DIContainer) resolveType(serviceType reflect.Type) (interface{}, error) {
139    service, err := c.Resolve(serviceType)
140    if err != nil {
141        return nil, err
142    }
143    return service, nil
144}
145
146// Auto-wire simple types
147func (c *DIContainer) autoWire(serviceType reflect.Type) (interface{}, error) {
148    if serviceType.Kind() != reflect.Struct {
149        return nil, fmt.Errorf("cannot auto-wire non-struct type: %v", serviceType)
150    }
151
152    value := reflect.New(serviceType).Elem()
153
154    // Try to inject fields
155    for i := 0; i < serviceType.NumField(); i++ {
156        field := serviceType.Field(i)
157        fieldTag := field.Tag.Get("inject")
158
159        if fieldTag != "" {
160            fieldValue, err := c.resolveType(field.Type)
161            if err != nil {
162                return nil, fmt.Errorf("failed to inject field %s: %v", field.Name, err)
163            }
164            value.Field(i).Set(reflect.ValueOf(fieldValue))
165
166            if c.options.DebugMode {
167                fmt.Printf("Auto-injected: %s.%s with %T\n", serviceType.Name(), field.Name, fieldValue)
168            }
169        }
170    }
171
172    return value.Interface(), nil
173}
174
175// Example service types and implementations
176type Logger interface {
177    Log(message string)
178}
179
180type Database interface {
181    Query(sql string) error
182}
183
184type UserService struct {
185    Logger   Logger   `inject:""`
186    Database Database `inject:""`
187}
188
189func (s *UserService) CreateUser(name string) error {
190    s.Logger.Log("Creating user: " + name)
191    return s.Database.Query("INSERT INTO users VALUES")
192}
193
194type ConsoleLogger struct{}
195
196func (c *ConsoleLogger) Log(message string) {
197    fmt.Printf("[LOG] %s\n", message)
198}
199
200type MockDatabase struct{}
201
202func (m *MockDatabase) Query(sql string) error {
203    fmt.Printf("[DB] Executing: %s\n", sql)
204    return nil
205}
206
207func main() {
208    fmt.Println("=== DI Container Implementation ===")
209
210    // Create container with auto-wire and debug
211    container := NewDIContainer(ContainerOptions{
212        AutoWire:   true,
213        StrictMode: false,
214        DebugMode:  true,
215    })
216
217    // Register implementations
218    var logger Logger = &ConsoleLogger{}
219    var database Database = &MockDatabase{}
220    container.Register(logger)
221    container.Register(database)
222
223    // Resolve user service
224    var userService UserService
225    resolved, err := container.ResolveByValue(&userService)
226    if err != nil {
227        fmt.Printf("Failed to resolve UserService: %v\n", err)
228        return
229    }
230
231    service := resolved.(*UserService)
232
233    // Use the service
234    if err := service.CreateUser("John Doe"); err != nil {
235        fmt.Printf("Error: %v\n", err)
236    } else {
237        fmt.Println("✅ User created successfully!")
238    }
239}

Expert Challenge - Framework Integration with Wire

Google's Wire generates dependency injection code at compile time.

  1// run
  2package main
  3
  4import (
  5    "fmt"
  6    "reflect"
  7    "strings"
  8)
  9
 10// Simplified Wire-like code generation
 11type WireGenerator struct {
 12    providers []ProviderInfo
 13    sets      []ProviderSet
 14}
 15
 16type ProviderInfo struct {
 17    Name     string
 18    Input    []reflect.Type
 19    Output   reflect.Type
 20    Function interface{}
 21}
 22
 23type ProviderSet struct {
 24    Name      string
 25    Providers []ProviderInfo
 26}
 27
 28func NewWireGenerator() *WireGenerator {
 29    return &WireGenerator{
 30        providers: make([]ProviderInfo, 0),
 31        sets:      make([]ProviderSet, 0),
 32    }
 33}
 34
 35func (w *WireGenerator) AddProvider(name string, function interface{}) {
 36    funcType := reflect.TypeOf(function)
 37    if funcType.Kind() != reflect.Func {
 38        panic("provider must be a function")
 39    }
 40
 41    inputs := make([]reflect.Type, funcType.NumIn())
 42    for i := 0; i < funcType.NumIn(); i++ {
 43        inputs[i] = funcType.In(i)
 44    }
 45
 46    provider := ProviderInfo{
 47        Name:     name,
 48        Input:    inputs,
 49        Output:   funcType.Out(0),
 50        Function: function,
 51    }
 52
 53    w.providers = append(w.providers, provider)
 54}
 55
 56func (w *WireGenerator) AddSet(name string, providers []ProviderInfo) {
 57    w.sets = append(w.sets, ProviderSet{
 58        Name:      name,
 59        Providers: providers,
 60    })
 61}
 62
 63func (w *WireGenerator) GenerateCode(injectorName string) string {
 64    var builder strings.Builder
 65
 66    // Header
 67    builder.WriteString("// Code generated by Wire Generator\n")
 68    builder.WriteString("// DO NOT EDIT\n\n")
 69    builder.WriteString("package main\n\n")
 70    builder.WriteString("import \"fmt\"\n\n")
 71
 72    // Generate injector function
 73    builder.WriteString(fmt.Sprintf("func %s() {\n", injectorName))
 74
 75    // Simplified generation - in real implementation, this would be much more complex
 76    for _, provider := range w.providers {
 77        if len(provider.Input) == 0 {
 78            // Constructor with no dependencies
 79            builder.WriteString(fmt.Sprintf("    %s := %s{}\n",
 80                strings.ToLower(provider.Name),
 81                provider.Name))
 82        }
 83    }
 84
 85    builder.WriteString("    fmt.Println(\"Generated injector executed\")\n")
 86    builder.WriteString("}\n\n")
 87
 88    return builder.String()
 89}
 90
 91func main() {
 92    fmt.Println("=== Wire-Like Code Generation ===")
 93
 94    generator := NewWireGenerator()
 95
 96    // Add providers
 97    generator.AddProvider("NewLogger", func() Logger {
 98        return &ConsoleLogger{}
 99    })
100
101    generator.AddProvider("NewDatabase", func() Database {
102        return &MockDatabase{}
103    })
104
105    generator.AddProvider("NewUserService", func(db Database, logger Logger) *UserService {
106        return &UserService{
107            Database: db,
108            Logger:   logger,
109        }
110    })
111
112    // Generate code
113    code := generator.GenerateCode("InitializeApp")
114
115    fmt.Println("Generated Code:")
116    fmt.Println(code)
117}

Practice Exercises

Exercise 1 - Refactor to Use Dependency Injection

Difficulty: Intermediate | Time: 45-60 minutes | Learning Objectives: Master dependency extraction, design interface-based architectures, and implement constructor injection patterns.

Refactor this tightly coupled code to use dependency injection by extracting dependencies into interfaces and passing them through constructors. This fundamental exercise teaches you to identify coupling issues in existing code and transform them into flexible, testable architectures. You'll learn to design clean abstractions, implement constructor injection patterns, and create separation between business logic and infrastructure concerns.

Solution
  1package main
  2
  3import (
  4    "fmt"
  5    "time"
  6)
  7
  8// ==== BAD: Tightly Coupled Code ====
  9
 10type TightlyCoupledService struct {
 11    // Hidden dependencies created internally
 12}
 13
 14func NewTightlyCoupledService() *TightlyCoupledService {
 15    // Hard-coded dependencies
 16    return &TightlyCoupledService{}
 17}
 18
 19func (s *TightlyCoupledService) ProcessData(data string) error {
 20    // Uses hidden dependencies - untestable!
 21    fmt.Println("Processing with hard-coded dependencies")
 22    return nil
 23}
 24
 25// ==== GOOD: Dependency Injected Code ====
 26
 27// Define interfaces for all dependencies
 28type Database interface {
 29    Query(query string) error
 30}
 31
 32type Logger interface {
 33    Log(message string)
 34}
 35
 36type Service struct {
 37    db     Database
 38    logger Logger
 39}
 40
 41func NewService(db Database, logger Logger) *Service {
 42    return &Service{
 43        db:     db,
 44        logger: logger,
 45    }
 46}
 47
 48func (s *Service) ProcessData(data string) error {
 49    s.logger.Log("Processing data: " + data)
 50
 51    err := s.db.Query("INSERT INTO data VALUES")
 52    if err != nil {
 53        s.logger.Log("Database error: " + err.Error())
 54        return err
 55    }
 56
 57    s.logger.Log("Data processed successfully")
 58    return nil
 59}
 60
 61// Implementations for production
 62type ProdDatabase struct {
 63    dsn string
 64}
 65
 66func NewProdDatabase(dsn string) Database {
 67    return &ProdDatabase{dsn: dsn}
 68}
 69
 70func (p *ProdDatabase) Query(query string) error {
 71    fmt.Printf("Production DB [%s]: %s\n", p.dsn, query)
 72    return nil
 73}
 74
 75type ProdLogger struct {
 76    level string
 77}
 78
 79func NewProdLogger(level string) Logger {
 80    return &ProdLogger{level: level}
 81}
 82
 83func (p *ProdLogger) Log(message string) {
 84    fmt.Printf("[%s] %s\n", p.level, message)
 85}
 86
 87// Implementations for testing
 88type TestDatabase struct{}
 89
 90func NewTestDatabase() Database {
 91    return &TestDatabase{}
 92}
 93
 94func (t *TestDatabase) Query(query string) error {
 95    fmt.Printf("Test DB: %s\n", query)
 96    return nil
 97}
 98
 99type TestLogger struct{}
100
101func NewTestLogger() Logger {
102    return &TestLogger{}
103}
104
105func (t *TestLogger) Log(message string) {
106    fmt.Printf("[TEST] %s\n", message)
107}
108
109func main() {
110    fmt.Println("=== Dependency Injection Refactoring ===")
111
112    // BAD: Tightly coupled usage
113    fmt.Println("\n--- BAD: Tightly Coupled ---")
114    badService := NewTightlyCoupledService()
115    badService.ProcessData("test data")
116
117    // GOOD: Dependency injection usage
118    fmt.Println("\n--- GOOD: Dependency Injected ---")
119
120    // Production usage
121    prodDB := NewProdDatabase("postgres://localhost/production")
122    prodLogger := NewProdLogger("INFO")
123    prodService := NewService(prodDB, prodLogger)
124    prodService.ProcessData("production data")
125
126    // Test usage
127    fmt.Println("\n--- TEST: Dependency Injected ---")
128    testDB := NewTestDatabase()
129    testLogger := NewTestLogger()
130    testService := NewService(testDB, testLogger)
131    testService.ProcessData("test data")
132}

Exercise 2 - Implement Functional Options Pattern

Difficulty: Advanced | Time: 60-90 minutes | Learning Objectives: Master functional options pattern, design flexible configuration APIs, and implement backwards-compatible configuration strategies.

Create a configurable HTTP client using the functional options pattern with timeout configuration, retry logic, custom headers, and logging capabilities. This exercise teaches you to design APIs that are both flexible and maintainable, allowing clients to configure behavior without constructor parameter explosion. You'll learn to implement configuration defaults, handle validation in options, and create APIs that can evolve without breaking existing code.

Solution
  1package main
  2
  3import (
  4    "fmt"
  5    "net/http"
  6    "time"
  7)
  8
  9type HTTPClient struct {
 10    timeout    time.Duration
 11    retries    int
 12    headers    map[string]string
 13    logger     Logger
 14    httpClient *http.Client
 15}
 16
 17type Logger interface {
 18    Debug(msg string)
 19    Info(msg string)
 20    Error(msg string)
 21}
 22
 23type HTTPOption func(*HTTPClient)
 24
 25// Option constructors
 26func WithTimeout(timeout time.Duration) HTTPOption {
 27    return func(c *HTTPClient) {
 28        c.timeout = timeout
 29    }
 30}
 31
 32func WithRetries(retries int) HTTPOption {
 33    return func(c *HTTPClient) {
 34        c.retries = retries
 35    }
 36}
 37
 38func WithHeader(key, value string) HTTPOption {
 39    return func(c *HTTPClient) {
 40        if c.headers == nil {
 41            c.headers = make(map[string]string)
 42        }
 43        c.headers[key] = value
 44    }
 45}
 46
 47func WithLogger(logger Logger) HTTPOption {
 48    return func(c *HTTPClient) {
 49        c.logger = logger
 50    }
 51}
 52
 53func WithUserAgent(agent string) HTTPOption {
 54    return WithHeader("User-Agent", agent)
 55}
 56
 57func WithAuthToken(token string) HTTPOption {
 58    return WithHeader("Authorization", "Bearer "+token)
 59}
 60
 61// Default logger
 62type DefaultLogger struct{}
 63
 64func (d *DefaultLogger) Debug(msg string) { fmt.Printf("[DEBUG] %s\n", msg) }
 65func (d *DefaultLogger) Info(msg string)  { fmt.Printf("[INFO] %s\n", msg) }
 66func (d *DefaultLogger) Error(msg string) { fmt.Printf("[ERROR] %s\n", msg) }
 67
 68func NewHTTPClient(opts ...HTTPOption) *HTTPClient {
 69    // Set defaults
 70    client := &HTTPClient{
 71        timeout: 30 * time.Second,
 72        retries: 3,
 73        headers: make(map[string]string),
 74        logger:  &DefaultLogger{},
 75    }
 76
 77    // Apply options
 78    for _, opt := range opts {
 79        opt(client)
 80    }
 81
 82    // Configure HTTP client
 83    client.httpClient = &http.Client{
 84        Timeout: client.timeout,
 85    }
 86
 87    return client
 88}
 89
 90func (c *HTTPClient) Get(url string) (*http.Response, error) {
 91    c.logger.Debug("Making GET request to: " + url)
 92
 93    var resp *http.Response
 94    var err error
 95
 96    for attempt := 0; attempt <= c.retries; attempt++ {
 97        if attempt > 0 {
 98            c.logger.Info(fmt.Sprintf("Retry attempt %d/%d", attempt, c.retries))
 99            time.Sleep(time.Duration(attempt) * time.Second)
100        }
101
102        req, err := http.NewRequest("GET", url, nil)
103        if err != nil {
104            return nil, err
105        }
106
107        // Apply headers
108        for key, value := range c.headers {
109            req.Header.Set(key, value)
110        }
111
112        resp, err = c.httpClient.Do(req)
113        if err == nil {
114            break
115        }
116
117        c.logger.Error("Request failed: " + err.Error())
118    }
119
120    if err != nil {
121        c.logger.Error("All retries failed: " + err.Error())
122        return nil, err
123    }
124
125    c.logger.Info(fmt.Sprintf("Request successful: %s", resp.Status))
126    return resp, nil
127}
128
129func main() {
130    fmt.Println("=== Functional Options Pattern ===")
131
132    // Default client
133    defaultClient := NewHTTPClient()
134    fmt.Println("\n--- Default Client ---")
135    resp, err := defaultClient.Get("https://httpbin.org/get")
136    if err != nil {
137        fmt.Printf("Error: %v\n", err)
138    } else if resp != nil {
139        fmt.Printf("Status: %s\n", resp.Status)
140        resp.Body.Close()
141    }
142
143    // Custom client with options
144    customClient := NewHTTPClient(
145        WithTimeout(10*time.Second),
146        WithRetries(5),
147        WithUserAgent("MyApp/1.0"),
148        WithHeader("Accept", "application/json"),
149        WithAuthToken("secret-token"),
150    )
151
152    fmt.Println("\n--- Custom Client ---")
153    resp, err = customClient.Get("https://httpbin.org/get")
154    if err != nil {
155        fmt.Printf("Error: %v\n", err)
156    } else if resp != nil {
157        fmt.Printf("Status: %s\n", resp.Status)
158        resp.Body.Close()
159    }
160}

Exercise 3 - Build a Simple DI Container

Difficulty: Expert | Time: 90-120 minutes | Learning Objectives: Master DI container implementation, understand type-safe dependency resolution, and build automatic dependency injection systems.

Implement a dependency injection container that can register and resolve services, handle singleton and transient lifetimes, detect circular dependencies, and provide automatic constructor injection. This expert-level exercise teaches you to build the core infrastructure used by major DI frameworks like Wire and Dig. You'll learn to implement reflection-based type systems, manage object lifetimes, and create dependency graphs that scale to complex applications.

Solution
  1package main
  2
  3import (
  4    "fmt"
  5    "reflect"
  6    "sync"
  7)
  8
  9type Lifetime int
 10
 11const (
 12    Transient Lifetime = iota
 13    Singleton
 14)
 15
 16type ServiceRegistration struct {
 17    Factory  interface{}
 18    Lifetime Lifetime
 19}
 20
 21type DIContainer struct {
 22    mu       sync.RWMutex
 23    services map[reflect.Type]ServiceRegistration
 24    instances map[reflect.Type]interface{}
 25    building map[reflect.Type]bool // Track for circular dependency detection
 26}
 27
 28func NewDIContainer() *DIContainer {
 29    return &DIContainer{
 30        services:  make(map[reflect.Type]ServiceRegistration),
 31        instances: make(map[reflect.Type]interface{}),
 32        building:  make(map[reflect.Type]bool),
 33    }
 34}
 35
 36func (c *DIContainer) RegisterTransient(factory interface{}) {
 37    c.mu.Lock()
 38    defer c.mu.Unlock()
 39
 40    factoryType := reflect.TypeOf(factory)
 41    if factoryType.Kind() != reflect.Func {
 42        panic("factory must be a function")
 43    }
 44
 45    serviceType := factoryType.Out(0)
 46    c.services[serviceType] = ServiceRegistration{
 47        Factory:  factory,
 48        Lifetime: Transient,
 49    }
 50}
 51
 52func (c *DIContainer) RegisterSingleton(factory interface{}) {
 53    c.mu.Lock()
 54    defer c.mu.Unlock()
 55
 56    factoryType := reflect.TypeOf(factory)
 57    if factoryType.Kind() != reflect.Func {
 58        panic("factory must be a function")
 59    }
 60
 61    serviceType := factoryType.Out(0)
 62    c.services[serviceType] = ServiceRegistration{
 63        Factory:  factory,
 64        Lifetime: Singleton,
 65    }
 66}
 67
 68func (c *DIContainer) RegisterInstance(instance interface{}) {
 69    c.mu.Lock()
 70    defer c.mu.Unlock()
 71
 72    serviceType := reflect.TypeOf(instance)
 73    c.services[serviceType] = ServiceRegistration{
 74        Factory:  instance, // Direct instance
 75        Lifetime: Singleton,
 76    }
 77    c.instances[serviceType] = instance
 78}
 79
 80func (c *DIContainer) Resolve(serviceType reflect.Type) (interface{}, error) {
 81    c.mu.Lock()
 82    defer c.mu.Unlock()
 83
 84    // Check for circular dependency
 85    if c.building[serviceType] {
 86        return nil, fmt.Errorf("circular dependency detected for type: %v", serviceType)
 87    }
 88
 89    // Check if already resolved for singletons
 90    if instance, exists := c.instances[serviceType]; exists {
 91        return instance, nil
 92    }
 93
 94    c.building[serviceType] = true
 95    defer func() {
 96        delete(c.building, serviceType)
 97    }()
 98
 99    // Get service registration
100    registration, exists := c.services[serviceType]
101    if !exists {
102        return nil, fmt.Errorf("service not registered: %v", serviceType)
103    }
104
105    var instance interface{}
106
107    switch factory := registration.Factory.(type) {
108    case func() interface{}:
109        instance = factory()
110    default:
111        // Direct instance registration
112        instance = registration.Factory
113    }
114
115    // Store singleton instances
116    if registration.Lifetime == Singleton {
117        c.instances[serviceType] = instance
118    }
119
120    return instance, nil
121}
122
123// Example services
124type Logger interface {
125    Log(message string)
126}
127
128type Database interface {
129    Query(query string) error
130}
131
132type UserService struct {
133    db     Database
134    logger Logger
135}
136
137func NewUserService(db Database, logger Logger) *UserService {
138    return &UserService{
139        db:     db,
140        logger: logger,
141    }
142}
143
144func (s *UserService) CreateUser(name string) error {
145    s.logger.Log("Creating user: " + name)
146    return s.db.Query("INSERT INTO users VALUES")
147}
148
149type ConsoleLogger struct{}
150
151func (c *ConsoleLogger) Log(message string) {
152    fmt.Printf("[LOG] %s\n", message)
153}
154
155type MockDatabase struct{}
156
157func (m *MockDatabase) Query(query string) error {
158    fmt.Printf("[DB] %s\n", query)
159    return nil
160}
161
162func main() {
163    fmt.Println("=== DI Container Implementation ===")
164
165    container := NewDIContainer()
166
167    // Register services
168    container.RegisterSingleton(func() Logger {
169        return &ConsoleLogger{}
170    })
171
172    container.RegisterSingleton(func() Database {
173        return &MockDatabase{}
174    })
175
176    // Resolve and use services
177    var logger Logger
178    loggerInterface, err := container.Resolve(reflect.TypeOf((*Logger)(nil)).Elem())
179    if err != nil {
180        fmt.Printf("Error: %v\n", err)
181        return
182    }
183    logger = loggerInterface.(Logger)
184
185    var db Database
186    dbInterface, err := container.Resolve(reflect.TypeOf((*Database)(nil)).Elem())
187    if err != nil {
188        fmt.Printf("Error: %v\n", err)
189        return
190    }
191    db = dbInterface.(Database)
192
193    // Create user service manually
194    userService := NewUserService(db, logger)
195    userService.CreateUser("Alice")
196
197    // Test singleton behavior
198    fmt.Println("\n--- Testing Singleton Behavior ---")
199    logger1Interface, _ := container.Resolve(reflect.TypeOf((*Logger)(nil)).Elem())
200    logger2Interface, _ := container.Resolve(reflect.TypeOf((*Logger)(nil)).Elem())
201    fmt.Printf("Same logger instance? %t\n", logger1Interface == logger2Interface)
202}

Exercise 4 - Implement Service Locator with Registration

Difficulty: Advanced | Time: 75-90 minutes | Learning Objectives: Master service locator pattern, implement type-safe registration, and understand trade-offs between DI and service locator.

Build a service locator that supports type-safe service registration and retrieval, lazy initialization, and dependency resolution with fallback strategies. This exercise teaches you to build centralized service management systems that balance convenience with dependency clarity. You'll learn to implement registration patterns, handle missing dependencies gracefully, and understand when service locators are appropriate versus constructor injection.

Solution
  1package main
  2
  3import (
  4    "fmt"
  5    "reflect"
  6    "sync"
  7)
  8
  9type ServiceLocator struct {
 10    mu        sync.RWMutex
 11    services  map[reflect.Type]interface{}
 12    factories map[reflect.Type]func() interface{}
 13}
 14
 15func NewServiceLocator() *ServiceLocator {
 16    return &ServiceLocator{
 17        services:  make(map[reflect.Type]interface{}),
 18        factories: make(map[reflect.Type]func() interface{}),
 19    }
 20}
 21
 22// Register an instance
 23func (sl *ServiceLocator) Register(service interface{}) {
 24    sl.mu.Lock()
 25    defer sl.mu.Unlock()
 26
 27    t := reflect.TypeOf(service)
 28    sl.services[t] = service
 29}
 30
 31// Register a factory for lazy initialization
 32func (sl *ServiceLocator) RegisterFactory(factory interface{}) {
 33    sl.mu.Lock()
 34    defer sl.mu.Unlock()
 35
 36    factoryFunc := reflect.ValueOf(factory)
 37    if factoryFunc.Kind() != reflect.Func {
 38        panic("factory must be a function")
 39    }
 40
 41    returnType := factoryFunc.Type().Out(0)
 42    sl.factories[returnType] = func() interface{} {
 43        return factoryFunc.Call(nil)[0].Interface()
 44    }
 45}
 46
 47// Get service by type
 48func (sl *ServiceLocator) Get(serviceType interface{}) (interface{}, bool) {
 49    t := reflect.TypeOf(serviceType).Elem()
 50
 51    sl.mu.RLock()
 52    service, exists := sl.services[t]
 53    sl.mu.RUnlock()
 54
 55    if exists {
 56        return service, true
 57    }
 58
 59    // Try factory
 60    sl.mu.RLock()
 61    factory, hasFactory := sl.factories[t]
 62    sl.mu.RUnlock()
 63
 64    if hasFactory {
 65        service := factory()
 66        sl.mu.Lock()
 67        sl.services[t] = service
 68        sl.mu.Unlock()
 69        return service, true
 70    }
 71
 72    return nil, false
 73}
 74
 75// Example services
 76type Logger interface {
 77    Log(message string)
 78}
 79
 80type Database interface {
 81    Query(sql string) error
 82}
 83
 84type Cache interface {
 85    Set(key, value string)
 86    Get(key string) (string, bool)
 87}
 88
 89type FileLogger struct{}
 90
 91func (f *FileLogger) Log(message string) {
 92    fmt.Println("[FILE]", message)
 93}
 94
 95type PostgresDB struct{}
 96
 97func (p *PostgresDB) Query(sql string) error {
 98    fmt.Println("[POSTGRES]", sql)
 99    return nil
100}
101
102type RedisCache struct {
103    data map[string]string
104}
105
106func NewRedisCache() *RedisCache {
107    fmt.Println("Initializing Redis cache...")
108    return &RedisCache{data: make(map[string]string)}
109}
110
111func (r *RedisCache) Set(key, value string) {
112    r.data[key] = value
113}
114
115func (r *RedisCache) Get(key string) (string, bool) {
116    val, ok := r.data[key]
117    return val, ok
118}
119
120// Service using locator
121type UserService struct {
122    locator *ServiceLocator
123}
124
125func NewUserService(locator *ServiceLocator) *UserService {
126    return &UserService{locator: locator}
127}
128
129func (s *UserService) CreateUser(name string) error {
130    // Get logger
131    loggerInterface, ok := s.locator.Get((*Logger)(nil))
132    if !ok {
133        return fmt.Errorf("logger not found")
134    }
135    logger := loggerInterface.(Logger)
136
137    // Get database
138    dbInterface, ok := s.locator.Get((*Database)(nil))
139    if !ok {
140        return fmt.Errorf("database not found")
141    }
142    db := dbInterface.(Database)
143
144    // Get cache (optional, with fallback)
145    cacheInterface, ok := s.locator.Get((*Cache)(nil))
146    if ok {
147        cache := cacheInterface.(Cache)
148        cache.Set("user:"+name, name)
149        logger.Log("Cached user: " + name)
150    }
151
152    logger.Log("Creating user: " + name)
153    return db.Query("INSERT INTO users VALUES")
154}
155
156func main() {
157    fmt.Println("=== Service Locator with Registration ===")
158
159    locator := NewServiceLocator()
160
161    // Register eager-loaded services
162    var logger Logger = &FileLogger{}
163    var db Database = &PostgresDB{}
164    locator.Register(logger)
165    locator.Register(db)
166
167    // Register factory for lazy loading
168    locator.RegisterFactory(func() Cache {
169        return NewRedisCache()
170    })
171
172    // Create and use service
173    service := NewUserService(locator)
174
175    fmt.Println("\n--- First Call (initializes cache) ---")
176    service.CreateUser("Alice")
177
178    fmt.Println("\n--- Second Call (reuses cache) ---")
179    service.CreateUser("Bob")
180}

Exercise 5 - Build Advanced DI Container with Scopes

Difficulty: Expert | Time: 120-150 minutes | Learning Objectives: Master scoped lifetimes, implement hierarchical containers, and build production-ready DI infrastructure.

Implement an advanced DI container that supports singleton, transient, and scoped lifetimes with child scopes for request-scoped dependencies. This capstone exercise teaches you to build enterprise-grade dependency injection systems that handle complex lifecycle requirements—essential for web applications where some dependencies should be shared per-request but not globally. You'll learn to implement scope hierarchies, manage parent-child relationships, and design disposal patterns for resource cleanup.

Solution
  1package main
  2
  3import (
  4    "fmt"
  5    "reflect"
  6    "sync"
  7)
  8
  9type Lifetime int
 10
 11const (
 12    Transient Lifetime = iota
 13    Singleton
 14    Scoped
 15)
 16
 17type ServiceRegistration struct {
 18    Factory  interface{}
 19    Lifetime Lifetime
 20}
 21
 22type ScopedContainer struct {
 23    parent    *DIContainer
 24    instances map[reflect.Type]interface{}
 25    mu        sync.RWMutex
 26}
 27
 28type DIContainer struct {
 29    mu            sync.RWMutex
 30    registrations map[reflect.Type]ServiceRegistration
 31    singletons    map[reflect.Type]interface{}
 32}
 33
 34func NewDIContainer() *DIContainer {
 35    return &DIContainer{
 36        registrations: make(map[reflect.Type]ServiceRegistration),
 37        singletons:    make(map[reflect.Type]interface{}),
 38    }
 39}
 40
 41func (c *DIContainer) Register(lifetime Lifetime, factory interface{}) {
 42    c.mu.Lock()
 43    defer c.mu.Unlock()
 44
 45    factoryType := reflect.TypeOf(factory)
 46    if factoryType.Kind() != reflect.Func {
 47        panic("factory must be a function")
 48    }
 49
 50    serviceType := factoryType.Out(0)
 51    c.registrations[serviceType] = ServiceRegistration{
 52        Factory:  factory,
 53        Lifetime: lifetime,
 54    }
 55}
 56
 57func (c *DIContainer) CreateScope() *ScopedContainer {
 58    return &ScopedContainer{
 59        parent:    c,
 60        instances: make(map[reflect.Type]interface{}),
 61    }
 62}
 63
 64func (c *DIContainer) Resolve(serviceType reflect.Type) (interface{}, error) {
 65    c.mu.RLock()
 66    registration, exists := c.registrations[serviceType]
 67    c.mu.RUnlock()
 68
 69    if !exists {
 70        return nil, fmt.Errorf("service not registered: %v", serviceType)
 71    }
 72
 73    switch registration.Lifetime {
 74    case Singleton:
 75        return c.resolveSingleton(serviceType, registration)
 76    case Transient:
 77        return c.createInstance(registration.Factory), nil
 78    default:
 79        return nil, fmt.Errorf("scoped services must be resolved from a scope")
 80    }
 81}
 82
 83func (c *DIContainer) resolveSingleton(serviceType reflect.Type, registration ServiceRegistration) (interface{}, error) {
 84    c.mu.RLock()
 85    if instance, ok := c.singletons[serviceType]; ok {
 86        c.mu.RUnlock()
 87        return instance, nil
 88    }
 89    c.mu.RUnlock()
 90
 91    instance := c.createInstance(registration.Factory)
 92
 93    c.mu.Lock()
 94    c.singletons[serviceType] = instance
 95    c.mu.Unlock()
 96
 97    return instance, nil
 98}
 99
100func (c *DIContainer) createInstance(factory interface{}) interface{} {
101    factoryValue := reflect.ValueOf(factory)
102    results := factoryValue.Call(nil)
103    return results[0].Interface()
104}
105
106func (sc *ScopedContainer) Resolve(serviceType reflect.Type) (interface{}, error) {
107    sc.parent.mu.RLock()
108    registration, exists := sc.parent.registrations[serviceType]
109    sc.parent.mu.RUnlock()
110
111    if !exists {
112        return nil, fmt.Errorf("service not registered: %v", serviceType)
113    }
114
115    switch registration.Lifetime {
116    case Singleton:
117        return sc.parent.Resolve(serviceType)
118
119    case Scoped:
120        sc.mu.RLock()
121        if instance, ok := sc.instances[serviceType]; ok {
122            sc.mu.RUnlock()
123            return instance, nil
124        }
125        sc.mu.RUnlock()
126
127        instance := sc.parent.createInstance(registration.Factory)
128
129        sc.mu.Lock()
130        sc.instances[serviceType] = instance
131        sc.mu.Unlock()
132
133        return instance, nil
134
135    case Transient:
136        return sc.parent.createInstance(registration.Factory), nil
137
138    default:
139        return nil, fmt.Errorf("unknown lifetime: %v", registration.Lifetime)
140    }
141}
142
143func (sc *ScopedContainer) Dispose() {
144    sc.mu.Lock()
145    defer sc.mu.Unlock()
146
147    for _, instance := range sc.instances {
148        if disposable, ok := instance.(interface{ Dispose() }); ok {
149            disposable.Dispose()
150        }
151    }
152
153    sc.instances = make(map[reflect.Type]interface{})
154}
155
156// Example services
157type Logger interface {
158    Log(message string)
159    GetID() int
160}
161
162type RequestLogger struct {
163    id int
164}
165
166var requestID int
167
168func NewRequestLogger() Logger {
169    requestID++
170    fmt.Printf("Creating RequestLogger %d\n", requestID)
171    return &RequestLogger{id: requestID}
172}
173
174func (r *RequestLogger) Log(message string) {
175    fmt.Printf("[Request-%d] %s\n", r.id, message)
176}
177
178func (r *RequestLogger) GetID() int {
179    return r.id
180}
181
182type Database interface {
183    Query(sql string) error
184}
185
186type DBConnection struct{}
187
188func NewDBConnection() Database {
189    fmt.Println("Creating DB connection")
190    return &DBConnection{}
191}
192
193func (d *DBConnection) Query(sql string) error {
194    fmt.Printf("[DB] %s\n", sql)
195    return nil
196}
197
198func main() {
199    fmt.Println("=== Advanced DI Container with Scopes ===")
200
201    container := NewDIContainer()
202
203    // Register singleton database
204    container.Register(Singleton, NewDBConnection)
205
206    // Register scoped logger (per-request)
207    container.Register(Scoped, NewRequestLogger)
208
209    // Simulate two requests
210    fmt.Println("\n--- Request 1 ---")
211    scope1 := container.CreateScope()
212    logger1a, _ := scope1.Resolve(reflect.TypeOf((*Logger)(nil)).Elem())
213    logger1b, _ := scope1.Resolve(reflect.TypeOf((*Logger)(nil)).Elem())
214    l1a := logger1a.(Logger)
215    l1b := logger1b.(Logger)
216    l1a.Log("First call in request 1")
217    l1b.Log("Second call in request 1")
218    fmt.Printf("Same logger in request 1? %t (ID: %d vs %d)\n",
219        l1a.GetID() == l1b.GetID(), l1a.GetID(), l1b.GetID())
220    scope1.Dispose()
221
222    fmt.Println("\n--- Request 2 ---")
223    scope2 := container.CreateScope()
224    logger2a, _ := scope2.Resolve(reflect.TypeOf((*Logger)(nil)).Elem())
225    logger2b, _ := scope2.Resolve(reflect.TypeOf((*Logger)(nil)).Elem())
226    l2a := logger2a.(Logger)
227    l2b := logger2b.(Logger)
228    l2a.Log("First call in request 2")
229    l2b.Log("Second call in request 2")
230    fmt.Printf("Same logger in request 2? %t (ID: %d vs %d)\n",
231        l2a.GetID() == l2b.GetID(), l2a.GetID(), l2b.GetID())
232    scope2.Dispose()
233
234    fmt.Println("\n--- Database is singleton ---")
235    db1, _ := container.Resolve(reflect.TypeOf((*Database)(nil)).Elem())
236    db2, _ := container.Resolve(reflect.TypeOf((*Database)(nil)).Elem())
237    fmt.Printf("Same database instance? %t\n", db1 == db2)
238}

Summary

💡 Core Mastery Achievements:

  1. DI Fundamentals

    • Mastered constructor injection and interface-based design
    • Understood dependency injection vs service locator patterns
    • Learned when DI adds value vs when it adds complexity
  2. Manual DI Patterns

    • Built explicit dependency graphs with clear contracts
    • Implemented functional options for flexible configuration
    • Designed clean architectures with proper separation of concerns
  3. Advanced Concepts

    • Implemented service locators and registry patterns
    • Built autowiring with reflection-based resolution
    • Managed dependency lifecycles (singleton, transient, scoped)
  4. Production Patterns

    • Created type-safe DI containers with lifecycle management
    • Implemented circular dependency detection
    • Built flexible, maintainable, and testable architectures
  5. Framework Integration

    • Generated dependency injection code at compile time
    • Used runtime DI containers for complex applications
    • Balanced flexibility, performance, and maintainability

When to Use Dependency Injection:

  • Multi-layer applications
  • Testable codebases where components need to be isolated
  • Configurable systems with multiple implementations
  • Production applications with complex dependency graphs

When DI Might Be Overkill:

  • ❌ Simple scripts or tools with minimal dependencies
  • ❌ Single-file applications without external systems
  • ❌ Prototypes or throwaway code
  • ❌ When all dependencies are simple and stable

DI vs Service Locator:

1// DI: Dependencies explicit in constructor
2service := NewUserService(db, logger)
3
4// Service Locator: Dependencies hidden and pulled when needed
5service := locator.Get("UserService") // Unclear dependencies!

Dependency Lifetimes:

  • Singleton: One instance for entire application
  • Transient: New instance for each request
  • Scoped: One instance per scope (e.g., per HTTP request)

Next Learning Path:

  1. Design Patterns - Master SOLID principles with DI
  2. Testing Strategies - Advanced mocking with DI
  3. Microservices - Apply DI patterns at scale
  4. Framework Design - Build your own DI framework

Real-World Impact: Dependency injection transforms monolithic, untestable code into modular, maintainable, and scalable architectures. You now have the knowledge to design systems that can evolve with changing requirements, be thoroughly tested, and handle complex real-world scenarios with confidence.

Dependency injection is not just a pattern—it's a fundamental approach to software architecture that enables clean, testable, and maintainable code. Start small with constructor injection, and gradually adopt more advanced techniques as your applications grow in complexity.