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:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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/)
|
||||
@@ -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/)
|
||||
Reference in New Issue
Block a user