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

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

  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
  • Middleware Pattern: Sets context values
  • Handler Pattern: Reads context values
  • Error Wrapping: Context cancellation errors

Further Reading