457 lines
11 KiB
Markdown
457 lines
11 KiB
Markdown
|
|
# 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)
|