Dependency Injection
A pattern where an object's dependencies are provided by an external entity rather than created by the object itself.
Contributors
When to Use
- When you want to decouple components from their specific implementations
- When you need to swap implementations for different environments (e.g. production vs. testing)
- When you want to improve code testability through mocking or stubbing
When NOT to Use
- For very simple applications where the overhead of interfaces adds unnecessary complexity
- When the dependencies are small, static, and unlikely to ever change
Dependency Injection (DI) is a design pattern that implements inversion of control for resolving dependencies. In Go, DI is typically achieved by passing interfaces to constructors or structs via main.go, allowing the caller to decide which implementation to provide.
Implementation
In this example, we define a DataStore interface and a UserService that depends on it.
package service
import (
"context"
"fmt"
)
// User represents a user entity
type User struct {
ID string
Name string
}
// DataStore defines the interface for data persistence
type DataStore interface {
User(ctx context.Context, id string) (*User, error)
}
// SQLDataStore is a concrete implementation of DataStore
type SQLDataStore struct {
ConnectionString string
}
func (s *SQLDataStore) User(ctx context.Context, id string) (*User, error) {
// In a real scenario, this would query a database
fmt.Printf("Fetching user %s from SQL database\n", id)
return &User{ID: id, Name: "John Doe"}, nil
}
// UserService depends on DataStore interface, not a concrete type
type UserService struct {
store DataStore
}
// NewUserService is a constructor that "injects" the dependency
func NewUserService(store DataStore) *UserService {
return &UserService{
store: store,
}
}
func (s *UserService) UserName(ctx context.Context, id string) (string, error) {
user, err := s.store.User(ctx, id)
if err != nil {
return "", err
}
return user.Name, nil
}
Usage
Wiring in Main
In production, we wire up the real implementations in the main function or a DI container.
package main
import (
"context"
"fmt"
"example/service"
)
func main() {
ctx := context.Background()
// 1. Create the concrete dependency
dbStore := &service.SQLDataStore{ConnectionString: "postgres://..."}
// 2. Inject it into the service
userService := service.NewUserService(dbStore)
// 3. Use the service
name, _ := userService.UserName(ctx, "123")
fmt.Println("User Name:", name)
}
Mocking for Tests
DI makes it trivial to swap the real database with a mock implementation during testing.
package service_test
import (
"context"
"testing"
"example/service"
)
// MockDataStore is a mock implementation of the DataStore interface
type MockDataStore struct {
UserFunc func(ctx context.Context, id string) (*service.User, error)
}
func (m *MockDataStore) User(ctx context.Context, id string) (*service.User, error) {
return m.UserFunc(ctx, id)
}
func TestUserService_UserName(t *testing.T) {
ctx := context.Background()
// Create a mock dependency
mockStore := &MockDataStore{
UserFunc: func(ctx context.Context, id string) (*service.User, error) {
return &service.User{ID: "test-id", Name: "Mock User"}, nil
},
}
// Inject the mock
userService := service.NewUserService(mockStore)
// Assert behavior
name, err := userService.UserName(ctx, "test-id")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if name != "Mock User" {
t.Errorf("expected 'Mock User', got '%s'", name)
}
}
Benefits
- Improved Testability: Dependencies can be easily mocked, allowing for isolated unit tests.
- Loose Coupling: Components don’t need to know about the internal details of their dependencies.
- Flexibility: Swapping implementations (e.g., from SQL to NoSQL or Mock) requires no changes to the service logic.
- Cleaner Construction: Logic for assembling the application is separated from the business logic.