# 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)