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:
-
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
-
Manual DI Patterns
- Build explicit dependency graphs with constructor injection
- Implement functional options for flexible configuration
- Design clean architectures with proper separation of concerns
-
Advanced DI Concepts
- Service locators and registry patterns
- Autowiring and automatic dependency resolution
- Dependency lifecycle management (singleton, transient, scoped)
-
Code Generation with Wire
- Use Google Wire for compile-time dependency injection
- Design provider sets and injection functions
- Build zero-runtime-overhead dependency systems
-
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:
-
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
-
Manual DI Patterns
- Built explicit dependency graphs with clear contracts
- Implemented functional options for flexible configuration
- Designed clean architectures with proper separation of concerns
-
Advanced Concepts
- Implemented service locators and registry patterns
- Built autowiring with reflection-based resolution
- Managed dependency lifecycles (singleton, transient, scoped)
-
Production Patterns
- Created type-safe DI containers with lifecycle management
- Implemented circular dependency detection
- Built flexible, maintainable, and testable architectures
-
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:
- Design Patterns - Master SOLID principles with DI
- Testing Strategies - Advanced mocking with DI
- Microservices - Apply DI patterns at scale
- 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.