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.
This commit is contained in:
@@ -0,0 +1,456 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user