← Alle Beiträge

Go Error Handling Patterns für Produktions-APIs: Jenseits einfacher Error Returns

Matthias Bruns · · 7 Min. Lesezeit
Go API Design Error Handling Production

Produktions-APIs verlangen kugelsichere Fehlerbehandlung. Während Go’s explizite Fehlerbehandlung einen guten Start bietet, erfordern resiliente Systeme ausgeklügelte Patterns, die weit über einfache if err != nil Checks hinausgehen. Dieser Guide behandelt erweiterte Error Handling Techniken, die Ihre Go APIs produktionstauglich machen – mit ordnungsgemäßer Observability, Debugging-Fähigkeiten und benutzerfreundlichen Antworten.

Das Fundament: Strukturierte Fehlertypen

Einfache Error-Strings reichen in der Produktion nicht aus. Sie brauchen strukturierte Fehler, die Kontext, Statuscodes und Metadaten transportieren. Hier ist ein robuster Fehlertyp, der das Fundament für produktionstaugliche Fehlerbehandlung bildet:

type APIError struct {
    Code       string            `json:"code"`
    Message    string            `json:"message"`
    StatusCode int               `json:"-"`
    Internal   error             `json:"-"`
    Fields     map[string]string `json:"fields,omitempty"`
    RequestID  string            `json:"request_id,omitempty"`
}

func (e *APIError) Error() string {
    if e.Internal != nil {
        return fmt.Sprintf("%s: %v", e.Message, e.Internal)
    }
    return e.Message
}

func (e *APIError) Unwrap() error {
    return e.Internal
}

Diese Struktur trennt öffentliche Nachrichten von internen Fehlerdetails – eine kritische Sicherheitspraxis. Wie JetBrains anmerkt, sollten Sie “Ihre Fehler in generische Nachrichten einpacken, wenn sie öffentliche Grenzen überschreiten, wie von Ihrem öffentlichen API Gateway zum Endnutzer.”

Error Wrapping und Kontext-Propagation

Go’s Error Wrapping Fähigkeiten glänzen, wenn Sie Fehler durch komplexe Call Stacks verfolgen müssen. Der Schlüssel ist, auf jeder Ebene Kontext hinzuzufügen, während der ursprüngliche Fehler erhalten bleibt:

func (s *UserService) GetUser(ctx context.Context, userID string) (*User, error) {
    user, err := s.repo.FindByID(ctx, userID)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            return nil, &APIError{
                Code:       "USER_NOT_FOUND",
                Message:    "Benutzer nicht gefunden",
                StatusCode: http.StatusNotFound,
                Internal:   fmt.Errorf("Benutzer-Lookup fehlgeschlagen für ID %s: %w", userID, err),
                RequestID:  GetRequestID(ctx),
            }
        }
        return nil, fmt.Errorf("user service: Fehler beim Abrufen von Benutzer %s: %w", userID, err)
    }
    return user, nil
}

Dieser Ansatz folgt Go’s Philosophie, dass Fehler behandelt oder an den Aufrufer weitergegeben werden sollten, bis eine Funktion weiter oben in der Call Chain den Fehler behandelt, während auf jeder Ebene wertvoller Kontext hinzugefügt wird.

HTTP Error Handler Pattern

Produktions-APIs brauchen konsistente Fehlerantworten. Implementieren Sie einen zentralen Error Handler, der interne Fehler in angemessene HTTP-Antworten übersetzt:

type ErrorHandler struct {
    logger *slog.Logger
}

func (h *ErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, err error) {
    requestID := GetRequestID(r.Context())
    
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        h.writeErrorResponse(w, apiErr, requestID)
        h.logError(r.Context(), apiErr)
        return
    }
    
    // Unbekannter Fehler - interne Details nicht preisgeben
    internalErr := &APIError{
        Code:       "INTERNAL_ERROR",
        Message:    "Interner Serverfehler",
        StatusCode: http.StatusInternalServerError,
        Internal:   err,
        RequestID:  requestID,
    }
    
    h.writeErrorResponse(w, internalErr, requestID)
    h.logError(r.Context(), internalErr)
}

func (h *ErrorHandler) writeErrorResponse(w http.ResponseWriter, err *APIError, requestID string) {
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Request-ID", requestID)
    w.WriteHeader(err.StatusCode)
    
    response := map[string]interface{}{
        "error": map[string]interface{}{
            "code":       err.Code,
            "message":    err.Message,
            "request_id": requestID,
        },
    }
    
    if err.Fields != nil {
        response["error"].(map[string]interface{})["fields"] = err.Fields
    }
    
    json.NewEncoder(w).Encode(response)
}

Custom Handler Wrapper

Eliminieren Sie repetitive Fehlerbehandlung in HTTP Handlers mit einem Wrapper-Pattern. Dieser Ansatz, inspiriert von Go’s offizieller Error Handling Anleitung, erstellt sauberere Handler-Funktionen:

type AppHandler func(http.ResponseWriter, *http.Request) error

func (fn AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        errorHandler.HandleError(w, r, err)
    }
}

// Verwendung in Handlers
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) error {
    userID := mux.Vars(r)["id"]
    if userID == "" {
        return &APIError{
            Code:       "INVALID_USER_ID",
            Message:    "Benutzer-ID ist erforderlich",
            StatusCode: http.StatusBadRequest,
        }
    }
    
    user, err := h.service.GetUser(r.Context(), userID)
    if err != nil {
        return err // Wrapper soll es behandeln
    }
    
    return json.NewEncoder(w).Encode(user)
}

// Registrierung mit dem Wrapper
http.Handle("/users/{id}", AppHandler(userHandler.GetUser))

Validierungs-Fehlerbehandlung

Eingabevalidierung verdient besondere Aufmerksamkeit in Produktions-APIs. Erstellen Sie spezielle Fehlertypen für Validierungsfehler:

type ValidationError struct {
    Field   string `json:"field"`
    Message string `json:"message"`
    Value   string `json:"value,omitempty"`
}

func NewValidationError(field, message, value string) *APIError {
    return &APIError{
        Code:       "VALIDATION_ERROR",
        Message:    "Validierung fehlgeschlagen",
        StatusCode: http.StatusBadRequest,
        Fields: map[string]string{
            field: message,
        },
    }
}

func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) error {
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        return &APIError{
            Code:       "INVALID_JSON",
            Message:    "Ungültiges JSON im Request Body",
            StatusCode: http.StatusBadRequest,
            Internal:   err,
        }
    }
    
    if req.Email == "" {
        return NewValidationError("email", "E-Mail ist erforderlich", "")
    }
    
    if !isValidEmail(req.Email) {
        return NewValidationError("email", "Ungültiges E-Mail-Format", req.Email)
    }
    
    // Weiter mit Business Logic...
}

Observability Integration

Produktionstaugliche Fehlerbehandlung muss sich in Observability-Systeme integrieren. Fügen Sie strukturiertes Logging, Metriken und Tracing hinzu:

func (h *ErrorHandler) logError(ctx context.Context, err *APIError) {
    logLevel := slog.LevelError
    if err.StatusCode < 500 {
        logLevel = slog.LevelWarn
    }
    
    attrs := []slog.Attr{
        slog.String("error_code", err.Code),
        slog.String("error_message", err.Message),
        slog.Int("status_code", err.StatusCode),
        slog.String("request_id", err.RequestID),
    }
    
    if err.Internal != nil {
        attrs = append(attrs, slog.String("internal_error", err.Internal.Error()))
    }
    
    if traceID := GetTraceID(ctx); traceID != "" {
        attrs = append(attrs, slog.String("trace_id", traceID))
    }
    
    h.logger.LogAttrs(ctx, logLevel, "API-Fehler aufgetreten", attrs...)
    
    // Metriken erhöhen
    h.incrementErrorMetric(err.Code, err.StatusCode)
}

func (h *ErrorHandler) incrementErrorMetric(code string, statusCode int) {
    // Beispiel mit Prometheus Metriken
    errorCounter.WithLabelValues(code, fmt.Sprintf("%d", statusCode)).Inc()
}

Circuit Breaker Error Handling

Wenn Ihre API von externen Services abhängt, implementieren Sie Circuit Breaker Patterns, um kaskadierende Ausfälle elegant zu behandeln:

type CircuitBreakerError struct {
    Service string
    State   string
}

func (e *CircuitBreakerError) Error() string {
    return fmt.Sprintf("Circuit Breaker %s für Service %s", e.State, e.Service)
}

func (s *PaymentService) ProcessPayment(ctx context.Context, req *PaymentRequest) error {
    err := s.circuitBreaker.Execute(func() error {
        return s.externalPaymentAPI.Process(ctx, req)
    })
    
    var cbErr *CircuitBreakerError
    if errors.As(err, &cbErr) {
        return &APIError{
            Code:       "SERVICE_UNAVAILABLE",
            Message:    "Zahlungsservice vorübergehend nicht verfügbar",
            StatusCode: http.StatusServiceUnavailable,
            Internal:   err,
        }
    }
    
    return err
}

Timeout und Context Error Handling

Context Cancellation und Timeouts erfordern spezielle Behandlung, um aussagekräftige Antworten zu liefern:

func (s *UserService) GetUserWithTimeout(ctx context.Context, userID string) (*User, error) {
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()
    
    user, err := s.repo.FindByID(ctx, userID)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, &APIError{
                Code:       "REQUEST_TIMEOUT",
                Message:    "Request-Timeout erreicht",
                StatusCode: http.StatusRequestTimeout,
                Internal:   err,
            }
        }
        
        if errors.Is(err, context.Canceled) {
            return nil, &APIError{
                Code:       "REQUEST_CANCELED",
                Message:    "Request wurde abgebrochen",
                StatusCode: 499, // Client closed request
                Internal:   err,
            }
        }
        
        return nil, fmt.Errorf("Fehler beim Abrufen des Benutzers: %w", err)
    }
    
    return user, nil
}

Error Recovery und Graceful Degradation

Implementieren Sie Fallback-Mechanismen für nicht-kritische Ausfälle:

func (s *RecommendationService) GetRecommendations(ctx context.Context, userID string) (*Recommendations, error) {
    // Primäre Recommendation Engine versuchen
    recs, err := s.primaryEngine.GetRecommendations(ctx, userID)
    if err == nil {
        return recs, nil
    }
    
    // Fehler loggen, aber mit Fallback fortfahren
    s.logger.WarnContext(ctx, "Primäre Recommendation Engine fehlgeschlagen, verwende Fallback",
        slog.String("user_id", userID),
        slog.String("error", err.Error()))
    
    // Fallback Engine versuchen
    fallbackRecs, fallbackErr := s.fallbackEngine.GetRecommendations(ctx, userID)
    if fallbackErr == nil {
        return fallbackRecs, nil
    }
    
    // Beide fehlgeschlagen - Fehler mit Kontext zurückgeben
    return nil, &APIError{
        Code:       "RECOMMENDATIONS_UNAVAILABLE",
        Message:    "Empfehlungen können nicht generiert werden",
        StatusCode: http.StatusServiceUnavailable,
        Internal:   fmt.Errorf("beide Engines fehlgeschlagen - primär: %w, fallback: %w", err, fallbackErr),
    }
}

Testen von Fehlerszenarien

Umfassende Fehlertests stellen sicher, dass Ihre Patterns unter Druck funktionieren:

func TestUserHandler_GetUser_NotFound(t *testing.T) {
    mockService := &MockUserService{}
    handler := &UserHandler{service: mockService}
    
    mockService.On("GetUser", mock.Anything, "123").Return(nil, &APIError{
        Code:       "USER_NOT_FOUND",
        Message:    "Benutzer nicht gefunden",
        StatusCode: http.StatusNotFound,
    })
    
    req := httptest.NewRequest("GET", "/users/123", nil)
    w := httptest.NewRecorder()
    
    err := handler.GetUser(w, req)
    
    assert.Error(t, err)
    
    var apiErr *APIError
    assert.True(t, errors.As(err, &apiErr))
    assert.Equal(t, "USER_NOT_FOUND", apiErr.Code)
    assert.Equal(t, http.StatusNotFound, apiErr.StatusCode)
}

Performance-Überlegungen

Fehlerbehandlung sollte die Performance nicht killen. Vermeiden Sie teure Operationen in Fehlerpfaden:

// Schlecht: Teure Stack Trace Generierung bei jedem Fehler
func badErrorHandler(err error) {
    stack := debug.Stack()
    log.Printf("Fehler mit Stack: %s\n%s", err, stack)
}

// Gut: Stack Traces nur bei schwerwiegenden Fehlern generieren
func goodErrorHandler(err error) {
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        if apiErr.StatusCode >= 500 {
            // Stack nur bei Server-Fehlern erfassen
            stack := debug.Stack()
            log.Printf("Server-Fehler: %s\nStack: %s", err, stack)
        } else {
            log.Printf("Client-Fehler: %s", err)
        }
    }
}

Resiliente Go APIs entwickeln

Diese Patterns verwandeln einfache Fehlerbehandlung in ein robustes System, das Produktionsoperationen unterstützt. Die wichtigsten Prinzipien sind:

  • Strukturieren Sie Ihre Fehler mit Codes, Nachrichten und Metadaten
  • Wrappen Sie Fehler, um Kontext zu bewahren und gleichzeitig Bedeutungsebenen hinzuzufügen
  • Zentralisieren Sie die Fehlerbehandlung, um konsistente Antworten sicherzustellen
  • Integrieren Sie Observability für Debugging und Monitoring
  • Planen Sie für Ausfälle mit Circuit Breakern und Graceful Degradation
  • Testen Sie Fehlerszenarien genauso gründlich wie Happy Paths

Go’s Ansatz, Fehler als Werte zu behandeln und explizite Behandlung zu fördern, macht diese Patterns natürlich und wartbar. Ihre APIs werden zuverlässiger, Ihre Debugging-Sessions kürzer und Ihre Benutzer zufriedener mit klaren, umsetzbaren Fehlermeldungen.

Die Investition in ausgeklügelte Fehlerbehandlung zahlt sich aus, wenn Probleme in der Produktion auftreten. Ihr zukünftiges Ich – und Ihr Operations-Team – wird Ihnen dafür danken, dass Sie Systeme gebaut haben, die elegant versagen und die Informationen liefern, die zur schnellen Problemlösung benötigt werden.

Lesebarkeit

Schriftgröße