Files
cv-site/doc/_go-learning/patterns/03-context-pattern.md
T

457 lines
11 KiB
Markdown
Raw Normal View History

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