算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
go-error-handling【设计与创意】Designs robust error handling in Go with custom error types, error wrapping, retry patterns, and failure recovery strategies for resilient applications.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
name: go-error-handling description: Designs robust error handling in Go with custom error types, error wrapping, retry patterns, and failure recovery strategies for resilient applications. license: MIT compatibility: opencode metadata: version: "1.0.0" domain: go role: implementation scope: implementation output-format: code content-types:
Senior engineer designing robust error handling in Go with custom error types, error wrapping, retry patterns, and failure recovery. This skill covers creating meaningful error hierarchies and implementing resilience patterns.
fmt.Errorf("...: %w", err)var ErrX = errors.New("...")errors.Is() and errors.As() for error inspection — never string comparisonUse this skill when:
Avoid this skill for:
panic() is appropriate (e.g., configuration that cannot be recovered from)Define Error Categories — Identify the types of errors your application produces. Checkpoint: Each error category has a sentinel error and a structured error type.
Implement Error Wrapping — Wrap errors at each layer with contextual information.
Checkpoint: Error chains are inspectable with errors.Is() and errors.As().
Build Retry Logic — Implement retry with exponential backoff for transient errors. Checkpoint: Retry distinguishes transient (retryable) from permanent (not retryable) errors.
Add Failure Recovery — Implement circuit breakers and fallbacks for critical paths. Checkpoint: Failed operations don't cascade — they fail gracefully with fallback behavior.
Propagate Errors Up — Wrap errors as they cross layer boundaries. Checkpoint: Each layer adds context without losing the underlying error.
Custom error types enable precise error handling across layer boundaries.
// ❌ BAD: string-based errors — impossible to pattern match, no context
func GetUser(id string) (*User, error) {
if id == "" {
return nil, errors.New("user id cannot be empty")
}
row := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
var user User
if err := row.Scan(&user.ID, &user.Name); err != nil {
// Generic error — caller can't distinguish "not found" from "connection error"
return nil, fmt.Errorf("query failed: %v", err)
}
return &user, nil
}
What's wrong:
"query failed" adds no useful contextstrings.Contains() — brittle and slow// package: internal/user
// Sentinel errors for common user-related conditions.
var (
// ErrUserNotFound is returned when a user does not exist.
ErrUserNotFound = errors.New("user not found")
// ErrUserDuplicate is returned when a user with the same identifier already exists.
ErrUserDuplicate = errors.New("user already exists")
// ErrUserInvalid is returned when user input fails validation.
ErrUserInvalid = errors.New("invalid user input")
)
// UserError is a structured error with a code and field information.
type UserError struct {
Code string // error code for observability
Message string // human-readable message
Fields map[string]string // field-level error details
Err error // underlying error (if any)
}
func (e *UserError) Error() string {
return e.Message
}
// Unwrap returns the underlying error for errors.Is/As support.
func (e *UserError) Unwrap() error {
return e.Err
}
// Is implements errors.Is for sentinel error matching.
func (e *UserError) Is(target error) bool {
_, ok := target.(*UserError)
return ok
}
// NewUserValidationError creates a validation error with field details.
func NewUserValidationError(field, message string) error {
return &UserError{
Code: "VALIDATION",
Message: fmt.Sprintf("user validation: %s: %s", field, message),
Fields: map[string]string{field: message},
}
}
// GetUser retrieves a user by ID with proper error categorization.
func GetUser(ctx context.Context, id string) (*User, error) {
if id == "" {
return nil, fmt.Errorf("get user: %w", ErrUserInvalid)
}
row := db.QueryRowContext(ctx, "SELECT id, name, email FROM users WHERE id = $1", id)
var user User
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("get user %s: %w", id, ErrUserNotFound)
}
return nil, fmt.Errorf("get user %s: query: %w", id, err)
}
return &user, nil
}
// CreateUser validates input and creates a new user.
func CreateUser(ctx context.Context, name, email string) (*User, error) {
var validationErrs []error
if name == "" {
validationErrs = append(validationErrs, NewUserValidationError("name", "name is required"))
}
if name != "" && len(name) > 100 {
validationErrs = append(validationErrs, NewUserValidationError("name", "name must be 100 characters or less"))
}
if email == "" {
validationErrs = append(validationErrs, NewUserValidationError("email", "email is required"))
}
if len(validationErrs) > 0 {
// Return a compound validation error
return nil, fmt.Errorf("create user: %v", multiError(validationErrs))
}
user := &User{
Name: strings.TrimSpace(name),
Email: strings.TrimSpace(email),
ID: generateID(),
}
if err := db.InsertUser(ctx, user); err != nil {
if isDuplicateKeyError(err) {
return nil, fmt.Errorf("create user: %w", ErrUserDuplicate)
}
return nil, fmt.Errorf("create user: %w", err)
}
return user, nil
}
Why this works:
ErrUserNotFound) are inspectable with errors.Is()UserError provides structured information (code, fields, underlying error)Unwrap() enables errors.Is() to traverse the error chain"get user %s: query: %w" → "create user: %w"Retry logic should be automatic, bounded, and error-aware.
// ❌ BAD: manual retry — no exponential backoff, no error classification
func fetchWithRetry(url string) ([]byte, error) {
var lastErr error
// Fixed 3 retries with fixed 1-second delay
for i := 0; i < 3; i++ {
resp, err := http.Get(url)
if err == nil {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return body, nil
}
lastErr = err
time.Sleep(1 * time.Second) // fixed delay — doesn't adapt
}
return nil, fmt.Errorf("fetch %s: all 3 retries failed: %w", url, lastErr)
}
What's wrong:
defer resp.Body.Close() in a loop — bodies are never closed until the function returns// RetryConfig holds retry configuration.
type RetryConfig struct {
MaxAttempts int // maximum number of attempts (default: 3)
InitialDelay time.Duration // delay before first retry (default: 100ms)
MaxDelay time.Duration // maximum delay between retries (default: 30s)
Jitter bool // add random jitter to delays (default: true)
Retryable func(error) bool // function to classify errors as retryable
}
// DefaultRetryConfig returns a retry config suitable for HTTP requests.
func DefaultRetryConfig() RetryConfig {
return RetryConfig{
MaxAttempts: 3,
InitialDelay: 100 * time.Millisecond,
MaxDelay: 30 * time.Second,
Jitter: true,
Retryable: isTransientError,
}
}
// Retry executes fn with exponential backoff until it succeeds or MaxAttempts is reached.
func Retry(ctx context.Context, cfg RetryConfig, fn func(ctx context.Context) error) error {
if cfg.MaxAttempts <= 0 {
cfg.MaxAttempts = 3
}
if cfg.InitialDelay == 0 {
cfg.InitialDelay = 100 * time.Millisecond
}
var lastErr error
for attempt := 0; attempt < cfg.MaxAttempts; attempt++ {
if attempt > 0 {
// Calculate delay with exponential backoff and jitter
delay := cfg.InitialDelay * time.Duration(1<<uint(attempt-1))
if delay > cfg.MaxDelay {
delay = cfg.MaxDelay
}
if cfg.Jitter {
delay = time.Duration(rand.Int63n(int64(delay)/2) + int64(delay)/4)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
}
}
lastErr = fn(ctx)
if lastErr == nil {
return nil // success
}
// Check if the error is retryable
if !cfg.Retryable(lastErr) {
return lastErr // permanent error — don't retry
}
}
return fmt.Errorf("all %d attempts failed: %w", cfg.MaxAttempts, lastErr)
}
// isTransientError classifies HTTP errors as transient (retryable) or permanent.
func isTransientError(err error) bool {
// Network errors are always retryable
if netErr, ok := err.(interface{ Temporary() bool }); ok && netErr.Temporary() {
return true
}
// HTTP status codes
var httpErr *httpError
if errors.As(err, &httpErr) {
switch httpErr.StatusCode {
case 429: // Too Many Requests — retry with backoff
return true
case 500, 502, 503, 504: // Server errors — retry
return true
case 400, 401, 403, 404, 405: // Client errors — don't retry
return false
default:
return false
}
}
return false
}
// httpError wraps an HTTP error with its status code.
type httpError struct {
StatusCode int
Body string
}
func (e *httpError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Body)
}
// fetchWithRetry fetches a URL with automatic retry for transient errors.
func fetchWithRetry(ctx context.Context, url string) ([]byte, error) {
cfg := DefaultRetryConfig()
var body []byte
err := Retry(ctx, cfg, func(ctx context.Context) error {
reqCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("request: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 10<<20))
if err != nil {
return fmt.Errorf("read body: %w", err)
}
if resp.StatusCode >= 500 {
return &httpError{StatusCode: resp.StatusCode, Body: string(respBody)}
}
if resp.StatusCode != 200 {
return nil // non-retryable error
}
body = respBody
return nil
})
if err != nil {
return nil, fmt.Errorf("fetch %s: %w", url, err)
}
return body, nil
}
Why this works:
Jitter adds randomness — prevents thundering herd when many clients retry simultaneouslyRetryable function classifies errors — only retries transient failuresMaxDelay caps the maximum wait — prevents extremely long delaysError categorization enables effective monitoring, alerting, and debugging.
// ❌ BAD: no error categories — all errors are just strings
func handleRequest(w http.ResponseWriter, r *http.Request) {
result, err := process(r)
if err != nil {
// No way to distinguish error types in monitoring
log.Printf("error: %v", err)
http.Error(w, "internal error", 500)
}
}
What's wrong:
"error: ..." — no error categorization for alerting// package: api
// ErrorCategory classifies errors for monitoring and alerting.
type ErrorCategory string
const (
CategoryValidation ErrorCategory = "validation"
CategoryNotFound ErrorCategory = "not_found"
CategoryConflict ErrorCategory = "conflict"
CategoryAuth ErrorCategory = "auth"
CategoryInternal ErrorCategory = "internal"
CategoryTimeout ErrorCategory = "timeout"
CategoryExternal ErrorCategory = "external"
)
// ErrorWithCategory wraps an error with a category for observability.
type ErrorWithCategory struct {
Category ErrorCategory
StatusCode int
Err error
}
func (e *ErrorWithCategory) Error() string {
return e.Err.Error()
}
func (e *ErrorWithCategory) Unwrap() error {
return e.Err
}
// NewErrorWithCategory creates a categorized error.
func NewErrorWithCategory(cat ErrorCategory, statusCode int, err error) *ErrorWithCategory {
return &ErrorWithCategory{
Category: cat,
StatusCode: statusCode,
Err: err,
}
}
// HandleError writes an appropriate HTTP response based on the error category.
func HandleError(w http.ResponseWriter, err error) {
// Extract category from error chain
var catErr *ErrorWithCategory
if errors.As(err, &catErr) {
// Log with category for monitoring
log.WithFields(log.Fields{
"category": catErr.Category,
"status": catErr.StatusCode,
"error": err.Error(),
}).Error("request error")
// Write appropriate HTTP status
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(catErr.StatusCode)
json.NewEncoder(w).Encode(map[string]string{
"error": err.Error(),
})
return
}
// Default: treat unknown errors as internal
log.WithFields(log.Fields{"error": err.Error()}).Error("unknown error")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{
"error": "internal server error",
})
}
// CategoryFromError determines the error category from an error.
func CategoryFromError(err error) (ErrorCategory, int) {
switch {
case errors.Is(err, user.ErrUserNotFound):
return CategoryNotFound, http.StatusNotFound
case errors.Is(err, user.ErrUserDuplicate):
return CategoryConflict, http.StatusConflict
case errors.Is(err, user.ErrUserInvalid):
return CategoryValidation, http.StatusBadRequest
case errors.Is(err, auth.ErrUnauthorized):
return CategoryAuth, http.StatusUnauthorized
case errors.Is(err, context.DeadlineExceeded):
return CategoryTimeout, http.StatusGatewayTimeout
default:
return CategoryInternal, http.StatusInternalServerError
}
}
// handleRequest processes a request with categorized error handling.
func handleRequest(w http.ResponseWriter, r *http.Request) {
result, err := process(r.Context())
if err != nil {
cat, statusCode := CategoryFromError(err)
HandleError(w, NewErrorWithCategory(cat, statusCode, err))
return
}
writeJSON(w, http.StatusOK, result)
}
Why this works:
"category=validation", "category=timeout", etc.CategoryFromError maps error types to categories — centralized classificationHandleError writes the correct HTTP status code based on error typefmt.Errorf("context: %w", err) to wrap errors — never lose the underlying errorerrors.Is() and errors.As() for error inspection — never string comparisonvar ErrX = errors.New("...")) for expected conditionsUnwrap() on custom error types to support errors.Is() and errors.As()panic() for error handling — only for truly unrecoverable conditions at startuperr must be checkedstrings.Contains(err.Error(), "...") for error matching| Skill | Purpose |
|---|---|
best-practices | Go error handling conventions and naming |
concurrency-patterns | Context cancellation and goroutine error handling |
cloud-development | Error handling in distributed systems with circuit breakers |