d95c62bad4
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.
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