docs: Complete _go-learning documentation with diagrams, patterns, and best practices

Added comprehensive educational documentation to fill empty folders:

## Architecture Diagrams (8 files)
- System architecture with layered design
- Complete request/response flow diagrams
- Middleware chain execution details
- Handler organization structure
- Data model relationships
- Error handling flows
- Template rendering pipeline
- PDF generation process with Chromedp

## Go Patterns (9 files)
- Pattern catalog and usage guide
- Middleware pattern (HTTP chain composition)
- Handler pattern (method-based organization)
- Context pattern (request-scoped values)
- Error wrapping (typed errors, chains)
- Dependency injection (constructor-based)
- Template pattern (rendering pipeline)
- Singleton pattern (thread-safe instances)
- Factory pattern (error/response constructors)

## Best Practices (2 files)
- Best practices catalog and quick reference
- Code organization (project structure, naming)

All documentation includes:
- Real examples from this project
- ASCII diagrams for visualization
- Best practices and anti-patterns
- Testing examples
- Performance considerations

Documentation structure:
- 20 markdown files
- ~6,000+ lines of educational content
- Cross-referenced between topics
- Practical, project-based examples
This commit is contained in:
juanatsap
2025-11-20 20:27:38 +00:00
parent faf3a2ca45
commit 219b83bfc0
9 changed files with 4920 additions and 0 deletions
+528
View File
@@ -0,0 +1,528 @@
# Handler Pattern in Go
## Pattern Overview
The Handler Pattern organizes HTTP endpoint logic into structured, testable components. This project uses a method-based handler approach where related endpoints are grouped as methods on a handler struct.
## Pattern Structure
```go
// Handler struct holds dependencies
type Handler struct {
tmpl *templates.Manager
db *database.DB
// other dependencies
}
// Constructor with dependency injection
func NewHandler(tmpl *templates.Manager, db *database.DB) *Handler {
return &Handler{
tmpl: tmpl,
db: db,
}
}
// HTTP handler methods
func (h *Handler) MethodName(w http.ResponseWriter, r *http.Request) {
// Handle request
}
```
## Real Implementation from Project
### CVHandler Structure
```go
// internal/handlers/cv.go
// CVHandler handles CV-related HTTP requests
type CVHandler struct {
tmpl *templates.Manager // Template renderer
host string // Server host for absolute URLs
}
// NewCVHandler creates a new CV handler with dependencies
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
return &CVHandler{
tmpl: tmpl,
host: host,
}
}
```
### Page Handlers
```go
// internal/handlers/cv_pages.go
// Home renders the main CV page
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Get user preferences from context (set by middleware)
prefs := middleware.GetPreferences(r)
// Get language from query params, fallback to preference
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = prefs.CVLanguage
}
// Validate language
if err := validateLanguage(lang); err != nil {
h.HandleError(w, r, err)
return
}
// Prepare template data
data, err := h.prepareTemplateData(lang)
if err != nil {
h.HandleError(w, r, err)
return
}
// Render template
if err := h.tmpl.Render(w, "index.html", data); err != nil {
h.HandleError(w, r, TemplateError(err))
return
}
}
// CVContent renders just the CV content (for HTMX partial updates)
func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
prefs := middleware.GetPreferences(r)
lang := prefs.CVLanguage
data, err := h.prepareTemplateData(lang)
if err != nil {
h.HandleError(w, r, err)
return
}
if err := h.tmpl.Render(w, "partials/cv_content.html", data); err != nil {
h.HandleError(w, r, TemplateError(err))
return
}
}
```
### HTMX Toggle Handlers
```go
// internal/handlers/cv_htmx.go
// ToggleCVLength toggles between short and long CV formats
func (h *CVHandler) ToggleCVLength(w http.ResponseWriter, r *http.Request) {
// Get current preferences from context
prefs := middleware.GetPreferences(r)
currentLength := prefs.CVLength
// Toggle state
newLength := "long"
if currentLength == "long" {
newLength = "short"
}
// Save new preference
middleware.SetPreferenceCookie(w, "cv-length", newLength)
// Render updated content
lang := middleware.GetLanguage(r)
data, err := h.prepareTemplateData(lang)
if err != nil {
h.HandleError(w, r, err)
return
}
if err := h.tmpl.Render(w, "partials/cv_content.html", data); err != nil {
h.HandleError(w, r, TemplateError(err))
return
}
}
// ToggleCVIcons toggles icon visibility
func (h *CVHandler) ToggleCVIcons(w http.ResponseWriter, r *http.Request) {
// Similar pattern: get → toggle → save → render
// ...
}
```
### Helper Methods
```go
// internal/handlers/cv_helpers.go
// prepareTemplateData loads and prepares all data for template rendering
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
// Load CV data
cv, err := cvmodel.LoadCV(lang)
if err != nil {
return nil, DataNotFoundError("CV", lang).WithErr(err)
}
// Load UI strings
ui, err := uimodel.LoadUI(lang)
if err != nil {
return nil, DataNotFoundError("UI", lang).WithErr(err)
}
// Calculate experience durations
for i := range cv.Experience {
cv.Experience[i].Duration = calculateDuration(
cv.Experience[i].StartDate,
cv.Experience[i].EndDate,
)
}
// Split skills into columns
skillColumns := splitSkillsIntoColumns(cv.Skills.Technical, 3)
// Build data map
return map[string]interface{}{
"CV": cv,
"UI": ui,
"SkillsColumns": skillColumns,
"PageTitle": fmt.Sprintf("%s - %s", cv.Personal.Name, cv.Personal.Title),
"CanonicalURL": h.getFullURL("/"),
}, nil
}
// getFullURL builds absolute URLs for SEO
func (h *CVHandler) getFullURL(path string) string {
return fmt.Sprintf("http://%s%s", h.host, path)
}
```
## Handler Organization by File
### Separation of Concerns
```
internal/handlers/
├── cv.go Constructor, shared state
├── cv_pages.go Full page renders (Home, CVContent)
├── cv_htmx.go HTMX partial updates (4 toggles)
├── cv_pdf.go PDF export endpoint
├── cv_helpers.go Shared utilities
├── types.go Request/response types
└── errors.go Error handling
```
This separation provides:
1. **Clear boundaries**: Each file has a specific purpose
2. **Easier navigation**: Find code by responsibility
3. **Better testing**: Test files mirror source files
4. **Reduced conflicts**: Multiple developers can work in parallel
## Route Registration
```go
// internal/routes/routes.go
func Setup(cvHandler *handlers.CVHandler, healthHandler *handlers.HealthHandler) http.Handler {
mux := http.NewServeMux()
// Page routes
mux.HandleFunc("/", cvHandler.Home)
mux.HandleFunc("/cv", cvHandler.CVContent)
// HTMX toggle routes
mux.HandleFunc("/toggle/length", cvHandler.ToggleCVLength)
mux.HandleFunc("/toggle/icons", cvHandler.ToggleCVIcons)
mux.HandleFunc("/toggle/theme", cvHandler.ToggleCVTheme)
mux.HandleFunc("/toggle/language", cvHandler.ToggleLanguage)
// PDF export route (with additional middleware)
pdfHandler := middleware.OriginChecker(
middleware.RateLimiter(
http.HandlerFunc(cvHandler.ExportPDF),
3, // 3 requests per minute
),
)
mux.Handle("/export/pdf", pdfHandler)
// Health check
mux.HandleFunc("/health", healthHandler.Health)
// Static files
fs := http.FileServer(http.Dir("static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
// Apply global middleware
handler := middleware.Recovery(
middleware.Logger(
middleware.SecurityHeaders(
middleware.PreferencesMiddleware(mux),
),
),
)
return handler
}
```
## Handler Benefits
### 1. Dependency Injection
```go
// Dependencies are explicit and injectable
type CVHandler struct {
tmpl *templates.Manager // Can be mocked
db *database.DB // Can be mocked
cache *redis.Client // Can be mocked
}
// Easy to test with mocks
func TestHome(t *testing.T) {
mockTmpl := &MockTemplateManager{}
handler := NewCVHandler(mockTmpl, "localhost:8080")
// Test with mock
}
```
### 2. Shared Logic
```go
// Helpers available to all handler methods
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
// Reused by Home(), CVContent(), ToggleCVLength(), etc.
}
func (h *CVHandler) HandleError(w http.ResponseWriter, r *http.Request, err error) {
// Centralized error handling for all methods
}
```
### 3. Context Access
```go
// All handler methods have access to:
// - Dependencies (h.tmpl, h.host)
// - Request (r)
// - Response (w)
func (h *CVHandler) AnyMethod(w http.ResponseWriter, r *http.Request) {
// Can access h.tmpl, h.host, etc.
}
```
## Alternative Handler Patterns
### 1. Function-Based Handlers
```go
// Simple approach for small apps
func Home(w http.ResponseWriter, r *http.Request) {
// No struct, just a function
// Dependencies passed as globals or closures
}
```
**When to use**: Very small apps, simple endpoints
**Drawbacks**: Hard to test, shared logic difficult, no dependency injection
### 2. Handler with Interface
```go
// Interface-based approach
type Handler interface {
Home(w http.ResponseWriter, r *http.Request)
Profile(w http.ResponseWriter, r *http.Request)
}
type CVHandler struct {
// ...
}
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// ...
}
```
**When to use**: Multiple implementations, complex testing
**Drawbacks**: More boilerplate, potentially over-engineered
### 3. Handler with http.Handler Interface
```go
// Implement http.Handler interface directly
type HomeHandler struct {
tmpl *templates.Manager
}
func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Handle request
}
// Register
mux.Handle("/", &HomeHandler{tmpl: tmplManager})
```
**When to use**: When you need to pass handlers around as interfaces
**Drawbacks**: One handler per endpoint, lots of small types
## Testing Handlers
### Unit Test Example
```go
// internal/handlers/cv_pages_test.go
func TestHome(t *testing.T) {
// Setup
cfg := &config.TemplateConfig{
Dir: "../../templates",
PartialsDir: "../../templates/partials",
HotReload: true,
}
tmplManager, err := templates.NewManager(cfg)
if err != nil {
t.Fatal(err)
}
handler := NewCVHandler(tmplManager, "localhost:8080")
// Create test request
req := httptest.NewRequest(http.MethodGet, "/?lang=en", nil)
w := httptest.NewRecorder()
// Execute
handler.Home(w, req)
// Verify
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
body := w.Body.String()
if !strings.Contains(body, "<!DOCTYPE html>") {
t.Error("response should be HTML")
}
}
```
### Table-Driven Tests
```go
func TestHome(t *testing.T) {
tests := []struct {
name string
lang string
wantStatus int
wantBody string
}{
{
name: "English version",
lang: "en",
wantStatus: http.StatusOK,
wantBody: "Professional Summary",
},
{
name: "Spanish version",
lang: "es",
wantStatus: http.StatusOK,
wantBody: "Resumen Profesional",
},
{
name: "Invalid language",
lang: "xx",
wantStatus: http.StatusBadRequest,
wantBody: "INVALID_LANGUAGE",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/?lang="+tt.lang, nil)
w := httptest.NewRecorder()
handler.Home(w, req)
if w.Code != tt.wantStatus {
t.Errorf("status = %d, want %d", w.Code, tt.wantStatus)
}
if !strings.Contains(w.Body.String(), tt.wantBody) {
t.Errorf("body missing %q", tt.wantBody)
}
})
}
}
```
## Best Practices
### ✅ DO
```go
// Keep handlers focused on HTTP concerns
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Parse request
// Validate input
// Call business logic
// Render response
}
// Extract business logic to helpers
func (h *CVHandler) prepareTemplateData(lang string) (map[string]interface{}, error) {
// This can be tested independently
}
// Use dependency injection
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
return &CVHandler{tmpl: tmpl, host: host}
}
// Group related handlers
type CVHandler struct {
// CV-related endpoints
}
type UserHandler struct {
// User-related endpoints
}
```
### ❌ DON'T
```go
// DON'T put business logic in handlers
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// 500 lines of business logic here...
}
// DON'T use global state
var globalTemplateManager *templates.Manager
// DON'T mix unrelated endpoints
type Handler struct {
// CV, Users, Orders, Payments all in one struct
}
// DON'T ignore errors
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
data, _ := h.prepareTemplateData(lang) // Ignoring error!
h.tmpl.Render(w, "index.html", data)
}
```
## Handler Testing Checklist
- [ ] Test happy path
- [ ] Test invalid input
- [ ] Test missing data
- [ ] Test error handling
- [ ] Test with different preferences/context
- [ ] Test response headers
- [ ] Test response status codes
- [ ] Test response body content
## Related Patterns
- **Dependency Injection**: Used in handler constructors
- **Middleware Pattern**: Wraps handlers for cross-cutting concerns
- **Context Pattern**: Request-scoped values in handlers
- **Error Wrapping**: Structured error handling in handlers
## Further Reading
- [HTTP Handler Pattern](https://www.alexedwards.net/blog/a-recap-of-request-handling)
- [Structuring Go Applications](https://www.gobeyond.dev/standard-package-layout/)
- [Dependency Injection](./05-dependency-injection.md)
+456
View File
@@ -0,0 +1,456 @@
# Context Pattern in Go
## Pattern Overview
The Context Pattern uses Go's `context` package to carry request-scoped values, cancellation signals, and deadlines across API boundaries and goroutines. It's the standard way to pass request-specific data through middleware chains to handlers.
## Pattern Structure
```go
// Store value in context
ctx := context.WithValue(parentCtx, key, value)
// Retrieve value from context
value := ctx.Value(key)
```
## Real Implementation from Project
### Storing Preferences in Context
```go
// internal/middleware/preferences.go
// PreferencesKey is the context key for user preferences
type contextKey string
const PreferencesKey contextKey = "preferences"
// PreferencesMiddleware reads cookies and stores in context
func PreferencesMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Read user preferences from cookies
prefs := &Preferences{
CVLength: getCookieWithDefault(r, "cv-length", "short"),
CVIcons: getCookieWithDefault(r, "cv-icons", "show"),
CVLanguage: getCookieWithDefault(r, "cv-language", "en"),
CVTheme: getCookieWithDefault(r, "cv-theme", "default"),
ColorTheme: getCookieWithDefault(r, "color-theme", "light"),
}
// Migrate old values
if prefs.CVLength == "extended" {
prefs.CVLength = "long"
}
// Store in context
ctx := context.WithValue(r.Context(), PreferencesKey, prefs)
// Pass modified context to next handler
next.ServeHTTP(w, r.WithContext(ctx))
})
}
```
### Retrieving Values from Context
```go
// internal/middleware/preferences.go
// GetPreferences retrieves preferences from request context
func GetPreferences(r *http.Request) *Preferences {
// Get value from context
prefs, ok := r.Context().Value(PreferencesKey).(*Preferences)
if !ok {
// Return defaults if not found
return &Preferences{
CVLength: "short",
CVIcons: "show",
CVLanguage: "en",
CVTheme: "default",
ColorTheme: "light",
}
}
return prefs
}
```
### Context Helper Functions
```go
// internal/middleware/preferences.go
// Convenience functions for cleaner code
// GetLanguage retrieves the user's language preference
func GetLanguage(r *http.Request) string {
return GetPreferences(r).CVLanguage
}
// GetCVLength retrieves the CV length preference
func GetCVLength(r *http.Request) string {
return GetPreferences(r).CVLength
}
// GetCVIcons retrieves the icons visibility preference
func GetCVIcons(r *http.Request) string {
return GetPreferences(r).CVIcons
}
// IsLongCV returns true if the user prefers long CV format
func IsLongCV(r *http.Request) bool {
return GetCVLength(r) == "long"
}
// ShowIcons returns true if icons should be visible
func ShowIcons(r *http.Request) bool {
return GetCVIcons(r) == "show"
}
```
### Using Context in Handlers
```go
// internal/handlers/cv_pages.go
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Easy access to preferences via helper
prefs := middleware.GetPreferences(r)
lang := prefs.CVLanguage
// Or use specific helpers
if middleware.IsLongCV(r) {
// Show long CV
}
if middleware.ShowIcons(r) {
// Include icons
}
// ...
}
```
## Context Key Best Practices
### Type-Safe Context Keys
```go
// ❌ BAD: String keys can collide
ctx := context.WithValue(ctx, "user", user)
// ✅ GOOD: Use custom type for keys
type contextKey string
const UserKey contextKey = "user"
ctx := context.WithValue(ctx, UserKey, user)
```
### Why Custom Types?
```go
// With string keys, these collide:
package auth
ctx := context.WithValue(ctx, "user", authUser)
package session
ctx := context.WithValue(ctx, "user", sessionUser) // Overwrites!
// With custom types, they're distinct:
package auth
type contextKey string
const UserKey contextKey = "user"
ctx := context.WithValue(ctx, UserKey, authUser)
package session
type contextKey string
const UserKey contextKey = "user"
ctx := context.WithValue(ctx, UserKey, sessionUser) // Different type!
```
## Context for Cancellation
### Handler with Timeout
```go
func (h *Handler) LongOperation(w http.ResponseWriter, r *http.Request) {
// Create context with timeout
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
// Use context in operation
result, err := h.doLongOperation(ctx)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "Operation timed out", http.StatusGatewayTimeout)
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
func (h *Handler) doLongOperation(ctx context.Context) (result interface{}, err error) {
// Check context before expensive operations
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// Do work...
return result, nil
}
```
### Database Query with Context
```go
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
// Pass request context to database
user, err := h.db.QueryContext(r.Context(), "SELECT * FROM users WHERE id = ?", userID)
if err != nil {
if errors.Is(err, context.Canceled) {
// Client disconnected
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
```
## Context Values vs. Function Parameters
### When to Use Context
```go
// ✅ GOOD: Request-scoped values
func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
prefs := middleware.GetPreferences(r) // From context
userID := middleware.GetUserID(r) // From context
// ...
}
// ✅ GOOD: Cancellation/timeouts
func doWork(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(1 * time.Second):
return nil
}
}
```
### When to Use Parameters
```go
// ✅ GOOD: Required function inputs
func calculateTotal(price float64, quantity int) float64 {
return price * float64(quantity)
}
// ✅ GOOD: Configuration
func NewHandler(config *Config, db *DB) *Handler {
return &Handler{config: config, db: db}
}
// ❌ BAD: Using context for function parameters
func calculateTotal(ctx context.Context) float64 {
price := ctx.Value("price").(float64) // Wrong!
quantity := ctx.Value("quantity").(int) // Wrong!
return price * float64(quantity)
}
```
## Common Context Patterns
### 1. Authentication
```go
// Middleware stores user in context
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
user, err := validateToken(token)
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), UserKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Handler retrieves user from context
func (h *Handler) Profile(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value(UserKey).(*User)
// Use user...
}
```
### 2. Request ID Tracing
```go
// Middleware generates and stores request ID
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestID := uuid.New().String()
ctx := context.WithValue(r.Context(), RequestIDKey, requestID)
// Add to response header
w.Header().Set("X-Request-ID", requestID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Use in logging
func logError(ctx context.Context, err error) {
requestID := ctx.Value(RequestIDKey).(string)
log.Printf("[%s] ERROR: %v", requestID, err)
}
```
### 3. Database Transaction
```go
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request) {
// Start transaction
tx, err := h.db.BeginTx(r.Context(), nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer tx.Rollback()
// Store transaction in context
ctx := context.WithValue(r.Context(), TxKey, tx)
// Call business logic with context
user, err := h.createUserWithTx(ctx, userData)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Commit transaction
if err := tx.Commit(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(user)
}
func (h *Handler) createUserWithTx(ctx context.Context, data UserData) (*User, error) {
// Get transaction from context
tx := ctx.Value(TxKey).(*sql.Tx)
// Use transaction
result, err := tx.ExecContext(ctx, "INSERT INTO users (...) VALUES (...)", ...)
// ...
}
```
## Context Anti-Patterns
### ❌ DON'T Store Context in Struct
```go
// BAD: Context in struct
type Handler struct {
ctx context.Context // Wrong!
}
// GOOD: Pass context as first parameter
func (h *Handler) DoWork(ctx context.Context) error {
// Use ctx here
}
```
### ❌ DON'T Use Context for Optional Parameters
```go
// BAD: Configuration in context
ctx := context.WithValue(ctx, "maxRetries", 3)
ctx = context.WithValue(ctx, "timeout", 10*time.Second)
doWork(ctx)
// GOOD: Use options pattern or struct
type Options struct {
MaxRetries int
Timeout time.Duration
}
doWork(ctx, Options{MaxRetries: 3, Timeout: 10*time.Second})
```
### ❌ DON'T Pass Context to Constructors
```go
// BAD: Context in constructor
func NewHandler(ctx context.Context, db *DB) *Handler {
return &Handler{ctx: ctx, db: db} // Wrong!
}
// GOOD: Accept context in methods
func NewHandler(db *DB) *Handler {
return &Handler{db: db}
}
func (h *Handler) DoWork(ctx context.Context) error {
// Use ctx here
}
```
## Testing with Context
```go
func TestHandler(t *testing.T) {
// Create test context with values
ctx := context.Background()
ctx = context.WithValue(ctx, PreferencesKey, &Preferences{
CVLength: "long",
})
// Create request with context
req := httptest.NewRequest(http.MethodGet, "/", nil)
req = req.WithContext(ctx)
// Test handler
w := httptest.NewRecorder()
handler.Home(w, req)
// Verify
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d", w.Code)
}
}
```
## Context Rules
1. **Always pass context as first parameter**: `func DoWork(ctx context.Context, ...)`
2. **Never store context in struct**: Pass it to methods
3. **Always call cancel**: `defer cancel()` after `context.WithTimeout/WithCancel`
4. **Check context.Done()**: In long-running operations
5. **Use custom types for keys**: Avoid string collisions
6. **Provide defaults**: When retrieving values from context
## Related Patterns
- **Middleware Pattern**: Sets context values
- **Handler Pattern**: Reads context values
- **Error Wrapping**: Context cancellation errors
## Further Reading
- [Go Context Package](https://golang.org/pkg/context/)
- [Context and HTTP](https://blog.golang.org/context)
- [Context Best Practices](https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39)
+558
View File
@@ -0,0 +1,558 @@
# Error Wrapping Pattern in Go
## Pattern Overview
Error Wrapping adds context to errors as they propagate up the call stack, creating a chain of errors that preserves both the original error and contextual information. Go 1.13+ provides `fmt.Errorf` with `%w` verb and `errors.Unwrap` for this pattern.
## Pattern Structure
```go
// Wrap error with context
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
// Unwrap to get original error
originalErr := errors.Unwrap(wrappedErr)
// Check if error chain contains specific error
if errors.Is(err, ErrNotFound) {
// Handle not found
}
// Extract error of specific type from chain
var domainErr *DomainError
if errors.As(err, &domainErr) {
// Use domain error
}
```
## Real Implementation from Project
### Basic Error Wrapping
```go
// internal/models/cv/cv.go
func LoadCV(lang string) (*CV, error) {
// Build file path
filePath := fmt.Sprintf("data/cv-%s.json", lang)
// Read file
data, err := os.ReadFile(filePath)
if err != nil {
// Wrap with context
return nil, fmt.Errorf("failed to read CV file: %w", err)
}
// Parse JSON
var cv CV
err = json.Unmarshal(data, &cv)
if err != nil {
// Wrap with more context
return nil, fmt.Errorf("failed to parse CV JSON: %w", err)
}
// Validate
if err := cv.Validate(); err != nil {
// Wrap validation error
return nil, fmt.Errorf("CV validation failed: %w", err)
}
return &cv, nil
}
```
### Domain Error Type
```go
// internal/handlers/errors.go
// DomainError represents application-level errors
type DomainError struct {
Code ErrorCode // Machine-readable error code
Message string // Human-readable message
Err error // Underlying error
StatusCode int // HTTP status code
Field string // Field that caused error
}
// Error implements error interface
func (e *DomainError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}
// Unwrap returns the underlying error
func (e *DomainError) Unwrap() error {
return e.Err
}
// WithErr adds underlying error
func (e *DomainError) WithErr(err error) *DomainError {
e.Err = err
return e
}
// WithField adds field information
func (e *DomainError) WithField(field string) *DomainError {
e.Field = field
return e
}
```
### Error Constructors
```go
// internal/handlers/errors.go
// InvalidLanguageError creates a language validation error
func InvalidLanguageError(lang string) *DomainError {
return NewDomainError(
ErrCodeInvalidLanguage,
fmt.Sprintf("Unsupported language: %s (use 'en' or 'es')", lang),
http.StatusBadRequest,
).WithField("lang")
}
// DataNotFoundError creates a data not found error
func DataNotFoundError(dataType, lang string) *DomainError {
return NewDomainError(
ErrCodeDataNotFound,
fmt.Sprintf("%s data not found for language: %s", dataType, lang),
http.StatusInternalServerError,
)
}
// PDFGenerationError creates a PDF generation error
func PDFGenerationError(err error) *DomainError {
return NewDomainError(
ErrCodePDFGeneration,
"Failed to generate PDF. Please try again.",
http.StatusInternalServerError,
).WithErr(err)
}
```
### Error Handling Chain
```go
// internal/handlers/cv_pages.go
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
// Validate language
if err := validateLanguage(lang); err != nil {
// err is already a DomainError
h.HandleError(w, r, err)
return
}
// Load CV data
cv, err := cvmodel.LoadCV(lang)
if err != nil {
// Wrap in DomainError with context
domErr := DataNotFoundError("CV", lang).WithErr(err)
h.HandleError(w, r, domErr)
return
}
// Render template
if err := h.tmpl.Render(w, "index.html", data); err != nil {
// Wrap template error
domErr := TemplateError(err)
h.HandleError(w, r, domErr)
return
}
}
```
### Centralized Error Handler
```go
// internal/handlers/errors.go
// HandleError processes errors and sends appropriate HTTP response
func (h *CVHandler) HandleError(w http.ResponseWriter, r *http.Request, err error) {
// Try to cast to DomainError
var domErr *DomainError
if errors.As(err, &domErr) {
// Handle domain error
h.handleDomainError(w, r, domErr)
return
}
// Check for specific errors
if errors.Is(err, context.Canceled) {
// Client disconnected
log.Printf("Request canceled: %v", err)
return
}
if errors.Is(err, context.DeadlineExceeded) {
// Timeout
domErr := NewDomainError(
ErrCodeTimeout,
"Request timed out",
http.StatusGatewayTimeout,
)
h.handleDomainError(w, r, domErr)
return
}
// Generic error
log.Printf("Unhandled error: %v", err)
domErr := NewDomainError(
ErrCodeInternalError,
"An unexpected error occurred",
http.StatusInternalServerError,
)
h.handleDomainError(w, r, domErr)
}
func (h *CVHandler) handleDomainError(w http.ResponseWriter, r *http.Request, domErr *DomainError) {
// Log error with code
log.Printf("[ERROR] %s: %s", domErr.Code, domErr.Message)
if domErr.Err != nil {
log.Printf("[ERROR] Underlying: %v", domErr.Err)
}
// Build error response
response := NewErrorResponse(
string(domErr.Code),
domErr.Message,
)
if domErr.Field != "" {
response.Error.Field = domErr.Field
}
// Send JSON response
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(domErr.StatusCode)
json.NewEncoder(w).Encode(response)
}
```
## Error Chain Example
### Full Error Propagation
```
1. File system error (os.ReadFile)
2. Wrapped by model (LoadCV)
"failed to read CV file: open data/cv-xx.json: no such file"
3. Wrapped by handler (Home)
DataNotFoundError("CV", "xx").WithErr(err)
4. Handled by error handler
{
"success": false,
"error": {
"code": "DATA_NOT_FOUND",
"message": "CV data not found for language: xx"
}
}
```
### Error Chain in Code
```go
// Layer 1: File system
_, err := os.ReadFile("data/cv-xx.json")
// err = &fs.PathError{Op:"open", Path:"data/cv-xx.json", Err:syscall.ENOENT}
// Layer 2: Model
if err != nil {
return nil, fmt.Errorf("failed to read CV file: %w", err)
}
// err = "failed to read CV file: open data/cv-xx.json: no such file or directory"
// Layer 3: Handler
if err != nil {
domErr := DataNotFoundError("CV", lang).WithErr(err)
}
// domErr = &DomainError{
// Code: "DATA_NOT_FOUND",
// Message: "CV data not found for language: xx",
// Err: [wrapped error from model],
// StatusCode: 500,
// }
// Layer 4: Error handler
h.HandleError(w, r, domErr)
// Logs full chain, sends user-friendly JSON
```
## Using errors.Is and errors.As
### errors.Is - Check Error Type
```go
func handleError(err error) {
// Check if error is or wraps specific error
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File not found")
return
}
if errors.Is(err, context.Canceled) {
fmt.Println("Request canceled")
return
}
if errors.Is(err, sql.ErrNoRows) {
fmt.Println("No data found")
return
}
fmt.Println("Unknown error:", err)
}
```
### errors.As - Extract Error Type
```go
func handleError(err error) {
// Extract DomainError from chain
var domErr *DomainError
if errors.As(err, &domErr) {
fmt.Printf("Domain error: code=%s, status=%d\n",
domErr.Code, domErr.StatusCode)
return
}
// Extract PathError from chain
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Printf("Path error: op=%s, path=%s\n",
pathErr.Op, pathErr.Path)
return
}
fmt.Println("Unknown error:", err)
}
```
## Custom Error Types
### Sentinel Errors
```go
// Define sentinel errors for comparison
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
// Use in code
if user == nil {
return ErrNotFound
}
// Check with errors.Is
if errors.Is(err, ErrNotFound) {
// Handle not found
}
```
### Error with Context
```go
// ValidationError includes field information
type ValidationError struct {
Field string
Message string
Value interface{}
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed for %s: %s (value: %v)",
e.Field, e.Message, e.Value)
}
// Usage
if len(name) == 0 {
return &ValidationError{
Field: "name",
Message: "name is required",
Value: name,
}
}
```
## Error Wrapping Best Practices
### ✅ DO
```go
// Add context when wrapping
if err != nil {
return fmt.Errorf("failed to process user %d: %w", userID, err)
}
// Use %w for wrapping (preserves error chain)
return fmt.Errorf("database query failed: %w", err)
// Wrap at each layer
func LoadUser(id int) (*User, error) {
data, err := readFile(fmt.Sprintf("users/%d.json", id))
if err != nil {
return nil, fmt.Errorf("load user %d: %w", id, err)
}
// ...
}
// Create custom errors with context
func InvalidEmailError(email string) error {
return fmt.Errorf("invalid email format: %s", email)
}
// Check errors with errors.Is
if errors.Is(err, os.ErrNotExist) {
// Handle file not found
}
// Extract errors with errors.As
var domErr *DomainError
if errors.As(err, &domErr) {
// Use domain error
}
```
### ❌ DON'T
```go
// DON'T use %v (loses error chain)
return fmt.Errorf("failed: %v", err) // Wrong!
return fmt.Errorf("failed: %w", err) // Correct
// DON'T ignore errors
data, _ := readFile(path) // Wrong!
// DON'T return generic errors
if invalid {
return errors.New("error") // Too generic!
}
// DON'T compare errors with ==
if err == someError { // Wrong! Use errors.Is
// ...
}
// DON'T type assert directly
domErr := err.(*DomainError) // Wrong! Use errors.As
```
## Error Logging
### Structured Logging
```go
func (h *Handler) processRequest(r *http.Request) error {
err := h.doWork()
if err != nil {
// Log with context
log.Printf("[ERROR] Request processing failed: %v", err)
// Log underlying errors
var domErr *DomainError
if errors.As(err, &domErr) {
log.Printf("[ERROR] Code: %s, Status: %d",
domErr.Code, domErr.StatusCode)
if domErr.Err != nil {
log.Printf("[ERROR] Underlying: %v", domErr.Err)
}
}
return err
}
return nil
}
```
### Stack Traces
```go
// For panics (recovered in middleware)
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// Log with stack trace
log.Printf("PANIC: %v\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error",
http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
```
## Testing Error Handling
```go
func TestLoadCV_FileNotFound(t *testing.T) {
// Test error wrapping
_, err := LoadCV("nonexistent")
// Check error occurred
if err == nil {
t.Fatal("expected error, got nil")
}
// Check error message contains context
if !strings.Contains(err.Error(), "failed to read CV file") {
t.Errorf("error missing context: %v", err)
}
// Check error chain contains specific error
if !errors.Is(err, os.ErrNotExist) {
t.Error("error should wrap os.ErrNotExist")
}
}
func TestHandleError_DomainError(t *testing.T) {
// Create domain error
domErr := InvalidLanguageError("xx")
// Test handling
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
handler.HandleError(w, req, domErr)
// Verify response
if w.Code != http.StatusBadRequest {
t.Errorf("status = %d, want %d", w.Code, http.StatusBadRequest)
}
var response APIResponse
json.NewDecoder(w.Body).Decode(&response)
if response.Success {
t.Error("expected success=false")
}
if response.Error.Code != "INVALID_LANGUAGE" {
t.Errorf("code = %s, want INVALID_LANGUAGE", response.Error.Code)
}
}
```
## Related Patterns
- **Handler Pattern**: Uses error wrapping for error handling
- **Context Pattern**: context.Canceled and context.DeadlineExceeded errors
- **Factory Pattern**: Error constructors create wrapped errors
## Further Reading
- [Go Error Handling](https://go.dev/blog/error-handling-and-go)
- [Working with Errors](https://go.dev/blog/go1.13-errors)
- [Error Handling Best Practices](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully)
@@ -0,0 +1,633 @@
# 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)
@@ -0,0 +1,636 @@
# Template Pattern in Go
## Pattern Overview
The Template Pattern (not to be confused with Go's `html/template` package) defines the skeleton of an algorithm in a method, deferring some steps to subclasses or functions. In Go, this is often implemented through interfaces and composition rather than inheritance.
In this project's context, we also use Go's template system which provides a different kind of template pattern for rendering HTML.
## Pattern Structure
```go
// Abstract template algorithm
type Processor interface {
Process() error
Validate() error
Transform() error
Save() error
}
// Concrete implementation
type DataProcessor struct {
// fields
}
func (p *DataProcessor) Process() error {
// Template method defines the algorithm
if err := p.Validate(); err != nil {
return err
}
if err := p.Transform(); err != nil {
return err
}
return p.Save()
}
// Steps can be customized
func (p *DataProcessor) Validate() error {
// Custom validation
}
```
## Real Implementation: Template Manager
### Template Manager Structure
```go
// internal/templates/manager.go
// Manager handles template rendering
type Manager struct {
templates map[string]*template.Template
config *config.TemplateConfig
mu sync.RWMutex
}
// NewManager creates and initializes template manager
func NewManager(config *config.TemplateConfig) (*Manager, error) {
m := &Manager{
templates: make(map[string]*template.Template),
config: config,
}
// Load templates on initialization
if err := m.loadTemplates(); err != nil {
return nil, err
}
return m, nil
}
```
### Template Loading Algorithm
```go
// loadTemplates follows a template algorithm pattern
func (m *Manager) loadTemplates() error {
// Step 1: Find template files
files, err := filepath.Glob(m.config.Dir + "/*.html")
if err != nil {
return fmt.Errorf("glob templates: %w", err)
}
// Step 2: For each template file
for _, file := range files {
name := filepath.Base(file)
// Step 3: Create new template
tmpl := template.New(name)
// Step 4: Add custom functions
tmpl = tmpl.Funcs(m.customFunctions())
// Step 5: Parse main template
tmpl, err = tmpl.ParseFiles(file)
if err != nil {
return fmt.Errorf("parse template %s: %w", name, err)
}
// Step 6: Parse partials
partialsPattern := filepath.Join(m.config.PartialsDir, "*.html")
tmpl, err = tmpl.ParseGlob(partialsPattern)
if err != nil {
return fmt.Errorf("parse partials: %w", err)
}
// Step 7: Cache template
m.templates[name] = tmpl
}
log.Printf("Loaded %d templates", len(m.templates))
return nil
}
```
### Template Rendering Algorithm
```go
// Render follows a consistent algorithm for all templates
func (m *Manager) Render(w io.Writer, name string, data interface{}) error {
// Step 1: Acquire read lock
m.mu.RLock()
defer m.mu.RUnlock()
// Step 2: Hot reload check (development)
if m.config.HotReload {
// Temporarily upgrade to write lock
m.mu.RUnlock()
m.mu.Lock()
m.loadTemplates() // Reload templates
m.mu.Unlock()
m.mu.RLock()
}
// Step 3: Get template from cache
tmpl, ok := m.templates[name]
if !ok {
return fmt.Errorf("template not found: %s", name)
}
// Step 4: Execute template
err := tmpl.Execute(w, data)
if err != nil {
return fmt.Errorf("template execution: %w", err)
}
return nil
}
```
### Custom Functions
```go
// customFunctions returns template helper functions
func (m *Manager) customFunctions() template.FuncMap {
return template.FuncMap{
// String manipulation
"lower": strings.ToLower,
"upper": strings.ToUpper,
"title": strings.Title,
// Date formatting
"formatDate": func(date string) string {
if date == "" {
return "Present"
}
t, err := time.Parse("2006-01", date)
if err != nil {
return date
}
return t.Format("Jan 2006")
},
// Collections
"join": strings.Join,
// Conditionals
"eq": func(a, b interface{}) bool {
return a == b
},
// HTML
"safe": func(s string) template.HTML {
return template.HTML(s)
},
}
}
```
## Template Method Pattern Example
### Data Processing Pipeline
```go
// DataProcessor defines template method
type DataProcessor struct {
data []byte
}
// Process is the template method (algorithm skeleton)
func (p *DataProcessor) Process() error {
// Step 1: Validate
if err := p.Validate(); err != nil {
return fmt.Errorf("validation: %w", err)
}
// Step 2: Parse
parsed, err := p.Parse()
if err != nil {
return fmt.Errorf("parsing: %w", err)
}
// Step 3: Transform
transformed, err := p.Transform(parsed)
if err != nil {
return fmt.Errorf("transform: %w", err)
}
// Step 4: Save
if err := p.Save(transformed); err != nil {
return fmt.Errorf("save: %w", err)
}
return nil
}
// Customizable steps
func (p *DataProcessor) Validate() error {
if len(p.data) == 0 {
return errors.New("empty data")
}
return nil
}
func (p *DataProcessor) Parse() (interface{}, error) {
var result interface{}
err := json.Unmarshal(p.data, &result)
return result, err
}
func (p *DataProcessor) Transform(data interface{}) (interface{}, error) {
// Transform logic
return data, nil
}
func (p *DataProcessor) Save(data interface{}) error {
// Save logic
return nil
}
```
### Interface-Based Template Method
```go
// Define steps as interface
type Validator interface {
Validate() error
}
type Parser interface {
Parse([]byte) (interface{}, error)
}
type Transformer interface {
Transform(interface{}) (interface{}, error)
}
// Pipeline uses interfaces for customization
type Pipeline struct {
validator Validator
parser Parser
transformer Transformer
}
func NewPipeline(v Validator, p Parser, t Transformer) *Pipeline {
return &Pipeline{
validator: v,
parser: p,
transformer: t,
}
}
// Process is template method
func (p *Pipeline) Process(data []byte) (interface{}, error) {
// Fixed algorithm, customizable steps
if err := p.validator.Validate(); err != nil {
return nil, err
}
parsed, err := p.parser.Parse(data)
if err != nil {
return nil, err
}
result, err := p.transformer.Transform(parsed)
if err != nil {
return nil, err
}
return result, nil
}
```
## Template Pattern in Handler Processing
### Request Processing Template
```go
// Handler follows template method for all requests
func (h *CVHandler) processRequest(
w http.ResponseWriter,
r *http.Request,
templateName string,
) error {
// Step 1: Get preferences (same for all)
prefs := middleware.GetPreferences(r)
// Step 2: Validate language (same for all)
lang := r.URL.Query().Get("lang")
if lang == "" {
lang = prefs.CVLanguage
}
if err := validateLanguage(lang); err != nil {
return err
}
// Step 3: Prepare data (same algorithm, different data)
data, err := h.prepareTemplateData(lang)
if err != nil {
return err
}
// Step 4: Render template (different template name)
if err := h.tmpl.Render(w, templateName, data); err != nil {
return TemplateError(err)
}
return nil
}
// Handlers use the template
func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
if err := h.processRequest(w, r, "index.html"); err != nil {
h.HandleError(w, r, err)
}
}
func (h *CVHandler) CVContent(w http.ResponseWriter, r *http.Request) {
if err := h.processRequest(w, r, "partials/cv_content.html"); err != nil {
h.HandleError(w, r, err)
}
}
```
## Function-Based Template Pattern
### Using Higher-Order Functions
```go
// Template function accepts customization functions
func ProcessWithTemplate(
validate func() error,
transform func() (interface{}, error),
save func(interface{}) error,
) error {
// Template algorithm
if err := validate(); err != nil {
return err
}
data, err := transform()
if err != nil {
return err
}
return save(data)
}
// Usage with closures
err := ProcessWithTemplate(
func() error {
// Custom validation
return validateInput(input)
},
func() (interface{}, error) {
// Custom transformation
return transformData(input)
},
func(data interface{}) error {
// Custom save
return db.Save(data)
},
)
```
## Template Caching Pattern
### Cache Management
```go
// Template cache with thread-safe access
type TemplateCache struct {
templates map[string]*template.Template
mu sync.RWMutex
}
// Get retrieves from cache (or loads if missing)
func (c *TemplateCache) Get(name string) (*template.Template, error) {
// Try read lock first
c.mu.RLock()
tmpl, ok := c.templates[name]
c.mu.RUnlock()
if ok {
return tmpl, nil
}
// Not found, load with write lock
c.mu.Lock()
defer c.mu.Unlock()
// Double-check after acquiring write lock
if tmpl, ok := c.templates[name]; ok {
return tmpl, nil
}
// Load template
tmpl, err := template.ParseFiles(name)
if err != nil {
return nil, err
}
// Cache it
c.templates[name] = tmpl
return tmpl, nil
}
```
## Benefits
1. **Consistency**: Algorithm is consistent across all uses
2. **Customization**: Steps can be customized without changing algorithm
3. **Code Reuse**: Common algorithm logic is reused
4. **Maintainability**: Changes to algorithm are centralized
5. **Testability**: Steps can be tested independently
## Real-World Use Cases
### 1. HTTP Request Processing
```go
// All requests follow same template
func (h *Handler) handleRequest(
w http.ResponseWriter,
r *http.Request,
process func() (interface{}, error),
) {
// 1. Authentication
user := authenticate(r)
// 2. Authorization
if !authorize(user, r) {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
// 3. Process (customizable)
result, err := process()
if err != nil {
h.handleError(w, err)
return
}
// 4. Respond
json.NewEncoder(w).Encode(result)
}
```
### 2. Data Migration
```go
// Migration template
type Migration interface {
Up() error
Down() error
}
type MigrationRunner struct {
migrations []Migration
}
func (r *MigrationRunner) Run() error {
for _, m := range r.migrations {
// Template: Begin → Execute → Commit/Rollback
tx := db.Begin()
if err := m.Up(); err != nil {
tx.Rollback()
return err
}
tx.Commit()
}
return nil
}
```
### 3. Test Setup/Teardown
```go
// Test template
type TestCase struct {
Name string
Setup func() error
Run func() error
Teardown func() error
}
func RunTestCase(tc *TestCase) error {
// Template algorithm
if err := tc.Setup(); err != nil {
return fmt.Errorf("setup: %w", err)
}
err := tc.Run()
// Always teardown, even on error
if teardownErr := tc.Teardown(); teardownErr != nil {
return fmt.Errorf("teardown: %w", teardownErr)
}
return err
}
```
## Best Practices
### ✅ DO
```go
// Define clear algorithm skeleton
func (p *Processor) Process() error {
if err := p.step1(); err != nil {
return err
}
if err := p.step2(); err != nil {
return err
}
return p.step3()
}
// Use interfaces for flexibility
type Step interface {
Execute() error
}
// Document the template algorithm
// Process executes the full processing pipeline:
// 1. Validate input
// 2. Transform data
// 3. Save result
func (p *Processor) Process() error {
// ...
}
// Make steps testable independently
func TestValidate(t *testing.T) {
p := &Processor{}
err := p.Validate()
// test validation logic
}
```
### ❌ DON'T
```go
// DON'T make algorithm too rigid
// Allow customization where appropriate
// DON'T mix concerns
// Keep template method focused on algorithm,
// not implementation details
// DON'T over-complicate
// If algorithm is simple, don't force template pattern
```
## Testing Template Methods
```go
func TestTemplateManager_Render(t *testing.T) {
// Test template algorithm
cfg := &config.TemplateConfig{
Dir: "testdata/templates",
PartialsDir: "testdata/partials",
HotReload: false,
}
manager, err := NewManager(cfg)
if err != nil {
t.Fatal(err)
}
// Test each step
t.Run("LoadTemplates", func(t *testing.T) {
if len(manager.templates) == 0 {
t.Error("expected templates to be loaded")
}
})
t.Run("Render", func(t *testing.T) {
var buf bytes.Buffer
data := map[string]string{"name": "Test"}
err := manager.Render(&buf, "test.html", data)
if err != nil {
t.Errorf("render failed: %v", err)
}
if buf.Len() == 0 {
t.Error("expected rendered output")
}
})
}
```
## Related Patterns
- **Strategy Pattern**: Both allow algorithm customization
- **Factory Pattern**: Often used with template for object creation
- **Handler Pattern**: Uses template method for request processing
## Further Reading
- [Template Method Pattern](https://refactoring.guru/design-patterns/template-method)
- [Go Templates](https://pkg.go.dev/text/template)
- [html/template Package](https://pkg.go.dev/html/template)
@@ -0,0 +1,601 @@
# Singleton Pattern in Go
## Pattern Overview
The Singleton Pattern ensures that a class has only one instance and provides a global point of access to it. In Go, this is typically achieved through package-level variables and `sync.Once` for thread-safe initialization.
## Pattern Structure
```go
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{
// initialization
}
})
return instance
}
```
## Real Implementation: Configuration Singleton
### Configuration Loading
```go
// internal/config/config.go
var (
instance *Config
once sync.Once
)
// Config holds application configuration
type Config struct {
Server ServerConfig
Templates TemplateConfig
}
// Load returns singleton configuration instance
func Load() *Config {
once.Do(func() {
instance = &Config{
Server: ServerConfig{
Host: getEnvOrDefault("HOST", "localhost"),
Port: getEnvOrDefault("PORT", ":8080"),
},
Templates: TemplateConfig{
Dir: getEnvOrDefault("TEMPLATE_DIR", "templates"),
PartialsDir: getEnvOrDefault("PARTIALS_DIR", "templates/partials"),
HotReload: getBoolEnv("HOT_RELOAD", true),
},
}
})
return instance
}
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
```
### Template Manager Singleton
```go
// In a larger application, template manager might be singleton
var (
templateManager *templates.Manager
tmplOnce sync.Once
)
func GetTemplateManager() (*templates.Manager, error) {
var err error
tmplOnce.Do(func() {
cfg := Load() // Get config singleton
templateManager, err = templates.NewManager(cfg.Templates)
})
return templateManager, err
}
```
## Thread-Safe Singleton
### Using sync.Once
```go
// sync.Once guarantees initialization happens exactly once
type Database struct {
conn *sql.DB
}
var (
db *Database
once sync.Once
)
func GetDatabase() (*Database, error) {
var err error
once.Do(func() {
db = &Database{}
db.conn, err = sql.Open("postgres", "connection-string")
if err != nil {
db = nil // Reset on error
}
})
if db == nil {
return nil, err
}
return db, nil
}
```
### Thread-Safety Comparison
```go
// ❌ NOT thread-safe
var instance *Singleton
func GetInstance() *Singleton {
if instance == nil { // Race condition!
instance = &Singleton{}
}
return instance
}
// ✅ Thread-safe with mutex (but slower)
var (
instance *Singleton
mu sync.Mutex
)
func GetInstance() *Singleton {
mu.Lock()
defer mu.Unlock()
if instance == nil {
instance = &Singleton{}
}
return instance
}
// ✅ Thread-safe with sync.Once (best)
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
```
## Singleton vs Package-Level Variables
### Simple Package-Level Variable
```go
// For simple, non-lazy initialization
package logger
var std = New(os.Stdout, InfoLevel)
func Info(msg string) {
std.Log(InfoLevel, msg)
}
func Debug(msg string) {
std.Log(DebugLevel, msg)
}
```
### When to Use Singleton vs Package Variable
**Use Singleton (sync.Once) when:**
- Initialization is expensive
- Initialization might fail
- Need lazy initialization
- Need thread-safe initialization
**Use Package Variable when:**
- Initialization is cheap
- Initialization always succeeds
- Want immediate initialization
- Simple, stateless utility
## Singleton Use Cases
### 1. Configuration
```go
// config/config.go
var (
cfg *Config
once sync.Once
)
func Load() *Config {
once.Do(func() {
cfg = &Config{}
// Load from file, env, etc.
cfg.loadFromEnv()
cfg.loadFromFile()
})
return cfg
}
```
### 2. Database Connection Pool
```go
// database/db.go
var (
pool *sql.DB
once sync.Once
)
func GetPool() (*sql.DB, error) {
var err error
once.Do(func() {
pool, err = sql.Open("postgres", getConnectionString())
if err != nil {
return
}
pool.SetMaxOpenConns(25)
pool.SetMaxIdleConns(5)
err = pool.Ping()
if err != nil {
pool.Close()
pool = nil
}
})
if pool == nil {
return nil, err
}
return pool, nil
}
```
### 3. Logger
```go
// logger/logger.go
var (
logger *Logger
once sync.Once
)
type Logger struct {
writer io.Writer
level Level
}
func Get() *Logger {
once.Do(func() {
logger = &Logger{
writer: os.Stdout,
level: InfoLevel,
}
})
return logger
}
// Convenience functions
func Info(msg string) {
Get().Log(InfoLevel, msg)
}
func Error(msg string) {
Get().Log(ErrorLevel, msg)
}
```
### 4. Cache
```go
// cache/cache.go
var (
cache *Cache
once sync.Once
)
type Cache struct {
data map[string]interface{}
mu sync.RWMutex
}
func Get() *Cache {
once.Do(func() {
cache = &Cache{
data: make(map[string]interface{}),
}
})
return cache
}
func Set(key string, value interface{}) {
c := Get()
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func Retrieve(key string) (interface{}, bool) {
c := Get()
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
```
## Anti-Pattern: Global State
### Problem
```go
// ❌ BAD: Mutable global state
var Config = &AppConfig{
Timeout: 30,
}
func main() {
Config.Timeout = 60 // Mutating global state
// Hard to test, unpredictable behavior
}
```
### Solution: Immutable Singleton
```go
// ✅ GOOD: Immutable singleton
var (
config *Config
once sync.Once
)
func GetConfig() *Config {
once.Do(func() {
config = &Config{
Timeout: 30,
}
})
return config // Read-only access
}
// To change config, create new instance
func WithTimeout(timeout int) *Config {
old := GetConfig()
return &Config{
Timeout: timeout,
// Copy other fields from old
}
}
```
## Testing Singletons
### Problem with Testing
```go
// Singleton makes testing difficult
func TestFeature(t *testing.T) {
instance := GetInstance()
instance.value = "test1"
// Test 1 passes
// But now instance.value is "test1" for next test!
}
```
### Solution: Reset for Tests
```go
// Add reset function for tests
func ResetForTest() {
once = sync.Once{}
instance = nil
}
func TestFeature(t *testing.T) {
defer ResetForTest()
instance := GetInstance()
instance.value = "test1"
// Test with clean state
}
```
### Alternative: Dependency Injection
```go
// Instead of singleton, use DI for testability
type Handler struct {
config *Config // Injected, not singleton
}
func NewHandler(config *Config) *Handler {
return &Handler{config: config}
}
// Easy to test with different configs
func TestHandler(t *testing.T) {
testConfig := &Config{Timeout: 10}
handler := NewHandler(testConfig)
// Test with test config
}
```
## Singleton Variations
### 1. Eager Initialization
```go
// Initialize at package load time
var instance = &Singleton{
// initialization
}
func GetInstance() *Singleton {
return instance
}
```
### 2. Lazy Initialization
```go
// Initialize on first use
var (
instance *Singleton
once sync.Once
)
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
```
### 3. With Error Handling
```go
var (
instance *Singleton
once sync.Once
err error
)
func GetInstance() (*Singleton, error) {
once.Do(func() {
instance, err = initialize()
})
return instance, err
}
func initialize() (*Singleton, error) {
s := &Singleton{}
if err := s.connect(); err != nil {
return nil, err
}
return s, nil
}
```
## Best Practices
### ✅ DO
```go
// Use sync.Once for thread-safety
var once sync.Once
// Make fields private
type Singleton struct {
privateField string
}
// Provide accessor methods
func (s *Singleton) GetValue() string {
return s.privateField
}
// Handle initialization errors
func GetInstance() (*Singleton, error) {
var err error
once.Do(func() {
instance, err = newSingleton()
})
return instance, err
}
// Document singleton nature
// GetDatabase returns the singleton database connection pool.
// Thread-safe and initialized lazily on first call.
func GetDatabase() *Database {
// ...
}
```
### ❌ DON'T
```go
// DON'T use mutable global state
var GlobalConfig Config // Mutable!
// DON'T forget thread-safety
if instance == nil { // Race condition!
instance = &Singleton{}
}
// DON'T make everything a singleton
// Only use for truly global, single-instance resources
// DON'T ignore errors in initialization
once.Do(func() {
instance, _ = newSingleton() // Ignoring error!
})
```
## When NOT to Use Singleton
1. **Testing is Important**: Dependency injection is better
2. **Multiple Instances Needed**: Use factory pattern
3. **State Changes**: Avoid mutable singletons
4. **Simple Utilities**: Use package functions
5. **Request-Scoped**: Use context pattern
## Alternatives to Singleton
### Dependency Injection
```go
// Better for testability
type Handler struct {
config *Config // Injected
db *DB // Injected
}
func NewHandler(config *Config, db *DB) *Handler {
return &Handler{config: config, db: db}
}
```
### Context Values
```go
// For request-scoped "singletons"
ctx := context.WithValue(ctx, ConfigKey, config)
// Retrieve in handler
config := ctx.Value(ConfigKey).(*Config)
```
### Package Functions
```go
// For stateless utilities
package mathutil
func Max(a, b int) int {
if a > b {
return a
}
return b
}
// No singleton needed
```
## Related Patterns
- **Dependency Injection**: Alternative to singleton
- **Factory Pattern**: Can create singletons
- **Multiton Pattern**: Multiple instances keyed by ID
## Further Reading
- [Singleton Pattern](https://refactoring.guru/design-patterns/singleton)
- [sync.Once Documentation](https://pkg.go.dev/sync#Once)
- [Go Singleton Best Practices](https://www.sohamkamani.com/golang/singleton-pattern/)
+659
View File
@@ -0,0 +1,659 @@
# Factory Pattern in Go
## Pattern Overview
The Factory Pattern provides an interface for creating objects without specifying the exact class of object that will be created. In Go, this is typically implemented through constructor functions that encapsulate complex object creation logic.
## Pattern Structure
```go
// Factory function
func NewObject(config Config) (*Object, error) {
// Complex initialization logic
obj := &Object{
field1: config.Value1,
field2: config.Value2,
}
// Validation
if err := obj.validate(); err != nil {
return nil, err
}
// Setup
if err := obj.initialize(); err != nil {
return nil, err
}
return obj, nil
}
```
## Real Implementation: Error Factories
### Domain Error Constructors
```go
// internal/handlers/errors.go
// NewDomainError is the base error factory
func NewDomainError(code ErrorCode, message string, statusCode int) *DomainError {
return &DomainError{
Code: code,
Message: message,
StatusCode: statusCode,
}
}
// Specific error factories
func InvalidLanguageError(lang string) *DomainError {
return NewDomainError(
ErrCodeInvalidLanguage,
fmt.Sprintf("Unsupported language: %s (use 'en' or 'es')", lang),
http.StatusBadRequest,
).WithField("lang")
}
func InvalidLengthError(length string) *DomainError {
return NewDomainError(
ErrCodeInvalidLength,
fmt.Sprintf("Invalid CV length: %s (use 'short' or 'long')", length),
http.StatusBadRequest,
).WithField("length")
}
func PDFGenerationError(err error) *DomainError {
return NewDomainError(
ErrCodePDFGeneration,
"Failed to generate PDF. Please try again.",
http.StatusInternalServerError,
).WithErr(err)
}
func DataNotFoundError(dataType, lang string) *DomainError {
return NewDomainError(
ErrCodeDataNotFound,
fmt.Sprintf("%s data not found for language: %s", dataType, lang),
http.StatusInternalServerError,
)
}
```
### Response Factories
```go
// internal/handlers/types.go
// NewAPIResponse creates a success response
func NewAPIResponse(data interface{}) *APIResponse {
return &APIResponse{
Success: true,
Data: data,
Meta: &MetaInfo{
Timestamp: time.Now(),
},
}
}
// NewErrorResponse creates an error response
func NewErrorResponse(code, message string) *APIResponse {
return &APIResponse{
Success: false,
Error: &ErrorInfo{
Code: code,
Message: message,
},
Meta: &MetaInfo{
Timestamp: time.Now(),
},
}
}
// NewPDFExportRequest creates a validated PDF export request
func NewPDFExportRequest() *PDFExportRequest {
return &PDFExportRequest{
Lang: "en",
Length: "short",
Icons: "show",
Version: "with_skills",
}
}
```
## Handler Factories
### CVHandler Factory
```go
// internal/handlers/cv.go
// NewCVHandler creates a new CV handler with all dependencies
func NewCVHandler(tmpl *templates.Manager, host string) *CVHandler {
return &CVHandler{
tmpl: tmpl,
host: host,
}
}
// With validation
func NewCVHandlerWithValidation(
tmpl *templates.Manager,
host string,
) (*CVHandler, error) {
if tmpl == nil {
return nil, errors.New("template manager is required")
}
if host == "" {
return nil, errors.New("host is required")
}
return &CVHandler{
tmpl: tmpl,
host: host,
}, nil
}
```
### Template Manager Factory
```go
// internal/templates/manager.go
// NewManager creates and initializes a template manager
func NewManager(config *config.TemplateConfig) (*Manager, error) {
// Validate config
if config == nil {
return nil, errors.New("config is required")
}
if config.Dir == "" {
return nil, errors.New("template directory is required")
}
// Create manager
m := &Manager{
templates: make(map[string]*template.Template),
config: config,
}
// Load templates
if err := m.loadTemplates(); err != nil {
return nil, fmt.Errorf("load templates: %w", err)
}
log.Printf("Template manager initialized with %d templates", len(m.templates))
return m, nil
}
```
## Factory with Options Pattern
### Functional Options
```go
// Option function type
type HandlerOption func(*Handler)
// Option constructors
func WithTimeout(d time.Duration) HandlerOption {
return func(h *Handler) {
h.timeout = d
}
}
func WithMaxRetries(n int) HandlerOption {
return func(h *Handler) {
h.maxRetries = n
}
}
func WithLogger(logger Logger) HandlerOption {
return func(h *Handler) {
h.logger = logger
}
}
// Factory with options
func NewHandler(tmpl *templates.Manager, opts ...HandlerOption) *Handler {
h := &Handler{
tmpl: tmpl,
timeout: 30 * time.Second, // Defaults
maxRetries: 3,
logger: &DefaultLogger{},
}
// Apply options
for _, opt := range opts {
opt(h)
}
return h
}
// Usage
handler := NewHandler(
tmplManager,
WithTimeout(10*time.Second),
WithMaxRetries(5),
WithLogger(customLogger),
)
```
### Options Struct
```go
// Options struct approach
type HandlerOptions struct {
Timeout time.Duration
MaxRetries int
Logger Logger
}
// DefaultOptions provides sensible defaults
func DefaultOptions() *HandlerOptions {
return &HandlerOptions{
Timeout: 30 * time.Second,
MaxRetries: 3,
Logger: &DefaultLogger{},
}
}
// Factory with options
func NewHandler(tmpl *templates.Manager, opts *HandlerOptions) *Handler {
if opts == nil {
opts = DefaultOptions()
}
return &Handler{
tmpl: tmpl,
timeout: opts.Timeout,
maxRetries: opts.MaxRetries,
logger: opts.Logger,
}
}
// Usage
handler := NewHandler(tmplManager, &HandlerOptions{
Timeout: 10 * time.Second,
MaxRetries: 5,
})
```
## Abstract Factory Pattern
### Database Factory
```go
// Database interface
type Database interface {
Query(query string) (Result, error)
Close() error
}
// Concrete implementations
type PostgresDB struct {
conn *sql.DB
}
type MySQLDB struct {
conn *sql.DB
}
type SQLiteDB struct {
conn *sql.DB
}
// Factory function
func NewDatabase(dbType, connString string) (Database, error) {
switch dbType {
case "postgres":
conn, err := sql.Open("postgres", connString)
if err != nil {
return nil, err
}
return &PostgresDB{conn: conn}, nil
case "mysql":
conn, err := sql.Open("mysql", connString)
if err != nil {
return nil, err
}
return &MySQLDB{conn: conn}, nil
case "sqlite":
conn, err := sql.Open("sqlite3", connString)
if err != nil {
return nil, err
}
return &SQLiteDB{conn: conn}, nil
default:
return nil, fmt.Errorf("unsupported database type: %s", dbType)
}
}
// Usage
db, err := NewDatabase("postgres", "connection-string")
```
## Factory with Builder Pattern
### Request Builder
```go
// Builder pattern for complex object construction
type RequestBuilder struct {
req *http.Request
err error
}
// NewRequestBuilder creates a new request builder
func NewRequestBuilder(method, url string) *RequestBuilder {
req, err := http.NewRequest(method, url, nil)
return &RequestBuilder{
req: req,
err: err,
}
}
// Builder methods
func (b *RequestBuilder) WithHeader(key, value string) *RequestBuilder {
if b.err != nil {
return b
}
b.req.Header.Set(key, value)
return b
}
func (b *RequestBuilder) WithBody(body io.Reader) *RequestBuilder {
if b.err != nil {
return b
}
b.req.Body = io.NopCloser(body)
return b
}
func (b *RequestBuilder) WithContext(ctx context.Context) *RequestBuilder {
if b.err != nil {
return b
}
b.req = b.req.WithContext(ctx)
return b
}
// Build finalizes and returns the request
func (b *RequestBuilder) Build() (*http.Request, error) {
return b.req, b.err
}
// Usage
req, err := NewRequestBuilder("POST", "https://api.example.com").
WithHeader("Content-Type", "application/json").
WithBody(bytes.NewBuffer(data)).
WithContext(ctx).
Build()
```
## Factory Method Pattern
### Data Loader Factory
```go
// Loader interface
type DataLoader interface {
Load(lang string) (interface{}, error)
}
// Concrete loaders
type CVLoader struct{}
func (l *CVLoader) Load(lang string) (interface{}, error) {
return cvmodel.LoadCV(lang)
}
type UILoader struct{}
func (l *UILoader) Load(lang string) (interface{}, error) {
return uimodel.LoadUI(lang)
}
// Factory method
func NewLoader(loaderType string) (DataLoader, error) {
switch loaderType {
case "cv":
return &CVLoader{}, nil
case "ui":
return &UILoader{}, nil
default:
return nil, fmt.Errorf("unknown loader type: %s", loaderType)
}
}
// Usage
loader, err := NewLoader("cv")
if err != nil {
return err
}
data, err := loader.Load("en")
```
## Factory Registry Pattern
### Handler Registry
```go
// Handler factory registry
type HandlerFactory func() http.Handler
var handlerRegistry = make(map[string]HandlerFactory)
// Register handler factory
func RegisterHandler(name string, factory HandlerFactory) {
handlerRegistry[name] = factory
}
// Get handler from registry
func GetHandler(name string) (http.Handler, error) {
factory, ok := handlerRegistry[name]
if !ok {
return nil, fmt.Errorf("handler not found: %s", name)
}
return factory(), nil
}
// Register handlers at init
func init() {
RegisterHandler("home", func() http.Handler {
return http.HandlerFunc(handleHome)
})
RegisterHandler("about", func() http.Handler {
return http.HandlerFunc(handleAbout)
})
}
// Usage
handler, err := GetHandler("home")
```
## Real-World Factory Examples
### 1. HTTP Client Factory
```go
// NewHTTPClient creates configured HTTP client
func NewHTTPClient(timeout time.Duration, maxRetries int) *http.Client {
return &http.Client{
Timeout: timeout,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
}
// With retry logic
func NewRetryableHTTPClient(timeout time.Duration, maxRetries int) *http.Client {
client := NewHTTPClient(timeout, maxRetries)
// Wrap with retry logic
return client
}
```
### 2. Logger Factory
```go
// Logger factory with different outputs
func NewLogger(output string) (*log.Logger, error) {
switch output {
case "stdout":
return log.New(os.Stdout, "[APP] ", log.LstdFlags), nil
case "stderr":
return log.New(os.Stderr, "[APP] ", log.LstdFlags), nil
case "file":
f, err := os.OpenFile("app.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
return log.New(f, "[APP] ", log.LstdFlags), nil
default:
return nil, fmt.Errorf("unknown output: %s", output)
}
}
```
### 3. Middleware Factory
```go
// Middleware factory
func NewAuthMiddleware(tokenValidator TokenValidator) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if err := tokenValidator.Validate(token); err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
}
// Usage
authMiddleware := NewAuthMiddleware(&JWTValidator{})
handler := authMiddleware(myHandler)
```
## Benefits
1. **Encapsulation**: Complex creation logic is hidden
2. **Consistency**: All objects created the same way
3. **Flexibility**: Easy to change implementation
4. **Testability**: Easy to create test objects
5. **Validation**: Centralized validation in factory
## Best Practices
### ✅ DO
```go
// Validate inputs in factory
func NewHandler(config *Config) (*Handler, error) {
if config == nil {
return nil, errors.New("config is required")
}
return &Handler{config: config}, nil
}
// Return errors for creation failures
func NewDatabase(connString string) (*Database, error) {
db, err := sql.Open("postgres", connString)
if err != nil {
return nil, fmt.Errorf("open database: %w", err)
}
return &Database{db: db}, nil
}
// Provide sensible defaults
func NewHandler(opts *Options) *Handler {
if opts == nil {
opts = DefaultOptions()
}
return &Handler{opts: opts}
}
// Use descriptive factory names
func NewRetryableHTTPClient(...) *http.Client
func NewCachedDatabase(...) *Database
func NewBufferedWriter(...) *Writer
```
### ❌ DON'T
```go
// DON'T return panics from factories
func NewHandler() *Handler {
config := loadConfig()
if config == nil {
panic("no config") // Wrong! Return error
}
return &Handler{config: config}
}
// DON'T ignore errors
func NewHandler() *Handler {
db, _ := connectDB() // Wrong! Handle error
return &Handler{db: db}
}
// DON'T make factories too complex
func NewHandler(...20 parameters...) *Handler {
// Too many parameters! Use options pattern
}
```
## Testing Factories
```go
func TestNewHandler(t *testing.T) {
t.Run("Valid config", func(t *testing.T) {
config := &Config{Timeout: 10}
handler, err := NewHandler(config)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if handler == nil {
t.Error("expected handler, got nil")
}
})
t.Run("Nil config", func(t *testing.T) {
handler, err := NewHandler(nil)
if err == nil {
t.Error("expected error for nil config")
}
if handler != nil {
t.Error("expected nil handler")
}
})
}
```
## Related Patterns
- **Builder Pattern**: For complex, multi-step object creation
- **Singleton Pattern**: Factories can create singletons
- **Dependency Injection**: Factories inject dependencies
## Further Reading
- [Factory Pattern](https://refactoring.guru/design-patterns/factory-method)
- [Functional Options](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis)
- [Go Constructor Patterns](https://www.sohamkamani.com/golang/options-pattern/)