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

634 lines
12 KiB
Markdown

# Dependency Injection Pattern in Go
## Pattern Overview
Dependency Injection (DI) is a pattern where dependencies are provided to a component rather than the component creating them itself. In Go, this is typically done through constructor functions that accept dependencies as parameters.
## Pattern Structure
```go
// Define dependencies as interfaces (optional but recommended)
type Database interface {
Query(query string) (Result, error)
}
// Component accepts dependencies via constructor
type Service struct {
db Database
logger Logger
config *Config
}
// Constructor injects dependencies
func NewService(db Database, logger Logger, config *Config) *Service {
return &Service{
db: db,
logger: logger,
config: config,
}
}
```
## Real Implementation from Project
### Handler with Dependencies
```go
// internal/handlers/cv.go
// CVHandler handles CV-related HTTP requests
type CVHandler struct {
tmpl *templates.Manager // Injected template manager
host string // Injected host configuration
}
// NewCVHandler creates a new CV handler with injected dependencies
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
return &CVHandler{
tmpl: tmpl,
host: host,
}
}
// Methods use injected dependencies
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Use injected template manager
if err := h.tmpl.Render(w, "index.html", data); err != nil {
// ...
}
// Use injected host for absolute URLs
canonicalURL := fmt.Sprintf("http://%s/", h.host)
}
```
### Template Manager with Dependencies
```go
// internal/templates/manager.go
// Manager handles template rendering
type Manager struct {
templates map[string]*template.Template
config *config.TemplateConfig // Injected configuration
mu sync.RWMutex
}
// NewManager creates template manager with injected config
func NewManager(config *config.TemplateConfig) (*Manager, error) {
m := &Manager{
templates: make(map[string]*template.Template),
config: config, // Store injected config
}
// Use config to load templates
if err := m.loadTemplates(); err != nil {
return nil, err
}
return m, nil
}
// Methods use injected config
func (m *Manager) loadTemplates() error {
// Use injected config
files, err := filepath.Glob(m.config.Dir + "/*.html")
// ...
}
```
### Main Function - Wiring Dependencies
```go
// main.go
func main() {
// Load configuration
cfg := config.Load()
// Create template manager (with config dependency)
tmplManager, err := templates.NewManager(cfg.Templates)
if err != nil {
log.Fatal(err)
}
// Create handlers (with template manager dependency)
cvHandler := handlers.NewCVHandler(tmplManager, cfg.Server.Host)
healthHandler := handlers.NewHealthHandler()
// Setup routes (with handler dependencies)
handler := routes.Setup(cvHandler, healthHandler)
// Start server
server := &http.Server{
Addr: cfg.Server.Port,
Handler: handler,
}
log.Printf("Server starting on %s", cfg.Server.Port)
if err := server.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
```
## Benefits of Dependency Injection
### 1. Testability
```go
// Without DI: Hard to test
type Handler struct {
// Creates dependencies internally
}
func NewHandler() *Handler {
db := database.Connect("prod-db") // Can't mock!
return &Handler{db: db}
}
// With DI: Easy to test
type Handler struct {
db Database // Interface
}
func NewHandler(db Database) *Handler {
return &Handler{db: db}
}
// Test with mock
func TestHandler(t *testing.T) {
mockDB := &MockDatabase{}
handler := NewHandler(mockDB)
// Test with mock
}
```
### 2. Flexibility
```go
// Switch implementations without changing handler code
// Production
realDB := &PostgresDB{conn: conn}
handler := NewHandler(realDB)
// Testing
mockDB := &MockDB{}
handler := NewHandler(mockDB)
// Development
localDB := &SQLiteDB{path: "dev.db"}
handler := NewHandler(localDB)
```
### 3. Explicit Dependencies
```go
// Clear what a component needs
func NewService(
db Database,
cache Cache,
logger Logger,
config *Config,
) *Service {
// Dependencies are explicit and visible
return &Service{
db: db,
cache: cache,
logger: logger,
config: config,
}
}
```
## Constructor Patterns
### 1. Simple Constructor
```go
// Direct initialization
func NewHandler(tmpl *templates.Manager, host string) *Handler {
return &Handler{
tmpl: tmpl,
host: host,
}
}
```
### 2. Constructor with Validation
```go
// Validate dependencies
func NewHandler(tmpl *templates.Manager, host string) (*Handler, error) {
if tmpl == nil {
return nil, errors.New("template manager is required")
}
if host == "" {
return nil, errors.New("host is required")
}
return &Handler{
tmpl: tmpl,
host: host,
}, nil
}
```
### 3. Constructor with Options
```go
// Options pattern for many optional dependencies
type HandlerOptions struct {
Host string
Timeout time.Duration
MaxRetries int
}
func NewHandler(tmpl *templates.Manager, opts *HandlerOptions) *Handler {
// Apply defaults
if opts == nil {
opts = &HandlerOptions{
Host: "localhost:8080",
Timeout: 30 * time.Second,
MaxRetries: 3,
}
}
return &Handler{
tmpl: tmpl,
host: opts.Host,
timeout: opts.Timeout,
maxRetries: opts.MaxRetries,
}
}
```
### 4. Functional Options
```go
// Functional options pattern
type HandlerOption func(*Handler)
func WithTimeout(d time.Duration) HandlerOption {
return func(h *Handler) {
h.timeout = d
}
}
func WithLogger(logger Logger) HandlerOption {
return func(h *Handler) {
h.logger = logger
}
}
func NewHandler(tmpl *templates.Manager, opts ...HandlerOption) *Handler {
h := &Handler{
tmpl: tmpl,
timeout: 30 * time.Second, // Default
}
// Apply options
for _, opt := range opts {
opt(h)
}
return h
}
// Usage
handler := NewHandler(
tmplManager,
WithTimeout(10*time.Second),
WithLogger(logger),
)
```
## Interface-Based DI
### Define Interfaces
```go
// Define interface for dependencies
type TemplateRenderer interface {
Render(w io.Writer, name string, data interface{}) error
}
type DataLoader interface {
LoadCV(lang string) (*CV, error)
LoadUI(lang string) (*UI, error)
}
// Handler depends on interfaces, not concrete types
type Handler struct {
tmpl TemplateRenderer
data DataLoader
}
func NewHandler(tmpl TemplateRenderer, data DataLoader) *Handler {
return &Handler{
tmpl: tmpl,
data: data,
}
}
```
### Benefits of Interfaces
```go
// Easy to mock for testing
type MockRenderer struct {
RenderCalled bool
RenderError error
}
func (m *MockRenderer) Render(w io.Writer, name string, data interface{}) error {
m.RenderCalled = true
return m.RenderError
}
// Test with mock
func TestHandler(t *testing.T) {
mock := &MockRenderer{}
handler := NewHandler(mock, nil)
// Test
handler.Home(w, r)
// Verify
if !mock.RenderCalled {
t.Error("expected Render to be called")
}
}
```
## Dependency Injection Patterns
### 1. Constructor Injection (Most Common in Go)
```go
type Service struct {
db Database
}
func NewService(db Database) *Service {
return &Service{db: db}
}
```
### 2. Method Injection (Less Common)
```go
type Service struct {
// No db field
}
func (s *Service) Process(db Database, data Data) error {
// db passed per-method call
return db.Save(data)
}
```
### 3. Property Injection (Avoid in Go)
```go
// Not idiomatic Go
type Service struct {
DB Database // Public field set after construction
}
service := &Service{}
service.DB = db // Set dependency manually - DON'T DO THIS
```
## Testing with Dependency Injection
### Mock Dependencies
```go
// internal/handlers/cv_pages_test.go
func TestHome(t *testing.T) {
// Create real template manager for test
cfg := &config.TemplateConfig{
Dir: "../../templates",
PartialsDir: "../../templates/partials",
HotReload: true,
}
tmplManager, err := templates.NewManager(cfg)
if err != nil {
t.Fatal(err)
}
// Inject into handler
handler := handlers.NewCVHandler(tmplManager, "localhost:8080")
// Test
req := httptest.NewRequest(http.MethodGet, "/?lang=en", nil)
w := httptest.NewRecorder()
handler.Home(w, req)
// Verify
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
```
### Test Doubles
```go
// Create test double that implements interface
type StubRenderer struct {
rendered bool
data interface{}
}
func (s *StubRenderer) Render(w io.Writer, name string, data interface{}) error {
s.rendered = true
s.data = data
fmt.Fprintf(w, "<html>Test</html>")
return nil
}
func TestWithStub(t *testing.T) {
stub := &StubRenderer{}
handler := NewHandler(stub, "test:8080")
handler.Home(w, req)
if !stub.rendered {
t.Error("expected template to be rendered")
}
}
```
## Dependency Injection Containers
Go doesn't have built-in DI containers like some languages, but libraries exist:
### Wire (Google)
```go
// wire.go
//go:build wireinject
import "github.com/google/wire"
func InitializeHandler() (*handlers.CVHandler, error) {
wire.Build(
config.Load,
templates.NewManager,
handlers.NewCVHandler,
)
return &handlers.CVHandler{}, nil
}
// Wire generates code at compile time
```
### Dig (Uber)
```go
import "go.uber.org/dig"
func main() {
container := dig.New()
// Register constructors
container.Provide(config.Load)
container.Provide(templates.NewManager)
container.Provide(handlers.NewCVHandler)
// Invoke
err := container.Invoke(func(h *handlers.CVHandler) {
// Use handler
})
}
```
### Manual Wiring (Recommended for Simple Apps)
```go
// main.go - Manual wiring is clear and simple
func main() {
cfg := config.Load()
tmpl, _ := templates.NewManager(cfg.Templates)
handler := handlers.NewCVHandler(tmpl, cfg.Server.Host)
// Clear dependency graph
}
```
## Best Practices
### ✅ DO
```go
// Accept dependencies via constructor
func NewHandler(db Database, logger Logger) *Handler {
return &Handler{db: db, logger: logger}
}
// Depend on interfaces, not concrete types
type Handler struct {
db Database // Interface
}
// Make dependencies explicit
func NewService(db Database, cache Cache, queue Queue) *Service {
// All dependencies visible in signature
}
// Validate dependencies
func NewHandler(db Database) (*Handler, error) {
if db == nil {
return nil, errors.New("database is required")
}
return &Handler{db: db}, nil
}
// Keep constructors simple
func NewHandler(tmpl *templates.Manager, host string) *Handler {
return &Handler{tmpl: tmpl, host: host}
}
```
### ❌ DON'T
```go
// DON'T create dependencies inside components
func NewHandler() *Handler {
db := connectDatabase() // Wrong! Hard to test
return &Handler{db: db}
}
// DON'T use global variables
var globalDB Database
func (h *Handler) Save() {
globalDB.Save() // Wrong! Hidden dependency
}
// DON'T make dependencies public
type Handler struct {
DB Database // Wrong! Should be private
}
// DON'T over-complicate with DI containers for simple apps
// Manual wiring in main() is often clearer
```
## Circular Dependencies
### Problem
```go
// ServiceA depends on ServiceB
type ServiceA struct {
b *ServiceB
}
// ServiceB depends on ServiceA
type ServiceB struct {
a *ServiceA
}
// Can't construct either!
```
### Solution: Interfaces
```go
// Break cycle with interface
type BInterface interface {
DoB()
}
type ServiceA struct {
b BInterface // Depends on interface
}
type ServiceB struct {
// No dependency on A
}
func (b *ServiceB) DoB() {}
// Can construct
b := &ServiceB{}
a := &ServiceA{b: b}
```
## Related Patterns
- **Handler Pattern**: Uses DI for template managers
- **Singleton Pattern**: Often combined with DI
- **Factory Pattern**: Can be used with DI
## Further Reading
- [Dependency Injection in Go](https://blog.drewolson.org/dependency-injection-in-go)
- [Google Wire](https://github.com/google/wire)
- [Uber Dig](https://github.com/uber-go/dig)