219b83bfc0
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
11 KiB
11 KiB
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
// 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
// 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
// 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
// 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
// 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
// ❌ 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?
// 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
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
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
// ✅ 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
// ✅ 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
// 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
// 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
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
// 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
// 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
// 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
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
- Always pass context as first parameter:
func DoWork(ctx context.Context, ...) - Never store context in struct: Pass it to methods
- Always call cancel:
defer cancel()aftercontext.WithTimeout/WithCancel - Check context.Done(): In long-running operations
- Use custom types for keys: Avoid string collisions
- Provide defaults: When retrieving values from context
Related Patterns
- Middleware Pattern: Sets context values
- Handler Pattern: Reads context values
- Error Wrapping: Context cancellation errors