d95c62bad4
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.
634 lines
12 KiB
Markdown
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)
|