Go Fehlerbehandlung 2026: Patterns, Wrapping und technische Interviewfragen

Fehlerbehandlung in Go unterscheidet sich grundlegend von anderen Sprachen. Dieser Artikel behandelt Error Wrapping, Sentinel Errors und typische Interviewfragen.

Go Error Handling Patterns

Go unterscheidet sich bei der Fehlerbehandlung grundlegend von den meisten anderen Programmiersprachen. Anstelle von Exceptions und try/catch-Blöcken behandelt Go Fehler als Werte, die explizit von Funktionen zurückgegeben und an jeder Aufrufstelle geprüft werden. Diese bewusste Designentscheidung rückt die Fehlerbehandlung in den Vordergrund und macht Fehlerpfade sichtbar sowie testbar.

Fehlerwerte statt Exceptions

Go besitzt keinen Exception-Mechanismus. Jede Funktion, die fehlschlagen kann, gibt einen Fehler als letzten Rückgabewert zurück. Dieses Muster macht die Kosten des Ignorierens von Fehlern auf Quelltextebene sichtbar und ermöglicht triviales Testen von Fehlerabläufen.

Das Error Interface und seine Bedeutung

Das gesamte Go-Fehlersystem basiert auf einem einzigen Interface, das in der Standardbibliothek definiert ist:

builtin.gogo
type error interface {
    Error() string
}

Jeder Typ, der Error() string implementiert, erfüllt dieses Interface. Diese Einfachheit fördert die Komponierbarkeit: Fehler können Structs, Wrapper oder beliebige andere Typen sein, solange sie eine String-Repräsentation liefern.

Ein einfacher benutzerdefinierter Fehlertyp demonstriert das Muster:

apperror.gogo
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}

Dieser NotFoundError trägt strukturierte Daten. Aufrufer können den Ressourcennamen oder die ID programmatisch extrahieren, anstatt einen String zu parsen. Dies ist ein entscheidender Vorteil beim Erstellen von HTTP-APIs oder CLI-Tools, die Fehler auf spezifische Antworten abbilden.

Sentinel Errors für bekannte Fehlerzustände

Sentinel Errors sind Variablen auf Paketebene, die spezifische, wohlbekannte Fehlerzustände repräsentieren. Die Standardbibliothek nutzt dieses Muster intensiv: io.EOF, sql.ErrNoRows, os.ErrNotExist.

errors.gogo
var (
    ErrNotFound     = errors.New("record not found")
    ErrUnauthorized = errors.New("unauthorized access")
    ErrConflict     = errors.New("resource conflict")
)

Sentinel Errors eignen sich am besten, wenn der Aufrufer nur wissen muss, was fehlgeschlagen ist, nicht warum im Detail. Sie signalisieren einen Zustand, ohne zusätzlichen Kontext zu tragen. Die Go-Konvention sieht vor, sie mit Err zu präfixieren und die Nachricht kleingeschrieben ohne Satzzeichen zu halten, gemäß den Go Code Review Comments.

Wenn Sentinels zum Problem werden

Das Exportieren von Sentinel Errors erzeugt einen öffentlichen API-Vertrag. Nachgelagerte Pakete werden sie mit errors.Is abgleichen, sodass das Umbenennen oder Entfernen eines Sentinel Errors eine Breaking Change darstellt. Sentinel Errors sollten sparsam eingesetzt werden, nur für Zustände, bei denen Aufrufer tatsächlich verzweigen müssen.

Error Wrapping mit fmt.Errorf und dem %w-Verb

Error Wrapping, eingeführt in Go 1.13, fügt einem Fehler Kontext hinzu, während der ursprüngliche Fehler in einer Kette erhalten bleibt. Das %w-Verb in fmt.Errorf erzeugt diese Kette:

repository.gogo
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    user, err := r.db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id)
    if err != nil {
        return nil, fmt.Errorf("UserRepo.FindByID(%s): %w", id, err)
    }
    return user, nil
}

Der gewrappte Fehler behält die vollständige Kette. Ein Aufrufer drei Schichten höher kann immer noch den ursprünglichen Sentinel abgleichen oder den ursprünglichen Typ extrahieren. Jede Schicht fügt Kontext darüber hinzu, wo der Fehler aufgetreten ist, ohne zu verschleiern, was passiert ist.

Eine kritische Unterscheidung: %w wrappt den Fehler und erhält die Kette, während %v den Fehler als String formatiert und die Kette unterbricht. %v sollte bewusst verwendet werden, wenn der ursprüngliche Fehler nicht durch die Abstraktionsgrenze durchdringen soll, beispielsweise wenn ein Repository einen Datenbanktreiber-Fehler wrappt, den der Service-Layer niemals inspizieren sollte.

Fehler untersuchen mit errors.Is und errors.As

Das errors-Paket stellt zwei Funktionen zur Untersuchung gewrappter Fehlerketten bereit, die direkten Vergleich und Type Assertion ersetzen.

errors.Is durchläuft die Kette auf der Suche nach einem spezifischen Fehlerwert:

handler.gogo
func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := userService.GetByID(r.Context(), chi.URLParam(r, "id"))
    if errors.Is(err, ErrNotFound) {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, "Internal error", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(user)
}

errors.As extrahiert einen spezifischen Fehlertyp aus der Kette:

middleware.gogo
func errorMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                http.Error(w, "Internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func mapErrorToHTTP(err error) int {
    var notFound *NotFoundError
    if errors.As(err, &notFound) {
        return http.StatusNotFound
    }
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        return http.StatusBadRequest
    }
    return http.StatusInternalServerError
}

Der entscheidende Vorteil gegenüber Type Assertions: Beide Funktionen durchlaufen die gesamte gewrappte Kette. Ein NotFoundError, der dreimal mit fmt.Errorf und %w gewrappt wurde, wird trotzdem erkannt.

Strukturierte Fehlerbehandlung in geschichteten Anwendungen

Produktionsreife Go-Anwendungen organisieren Fehler typischerweise über drei Schichten: Domain-Fehler definieren Zustände auf Geschäftslogikebene, Service-Fehler fügen operativen Kontext hinzu, und Handler- oder Transport-Fehler bilden sie auf externe Antworten ab.

domain/errors.gogo
type DomainError struct {
    Code    string
    Message string
    Err     error
}

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

func (e *DomainError) Unwrap() error {
    return e.Err
}

Die Unwrap-Methode ermöglicht es errors.Is und errors.As, durch die Kette zu navigieren. Jeder benutzerdefinierte Fehlertyp, der einen anderen Fehler wrappt, sollte diese Methode implementieren.

Der Service-Layer wrappt Domain-Fehler mit operativem Kontext:

service/user.gogo
func (s *UserService) Deactivate(ctx context.Context, userID string) error {
    user, err := s.repo.FindByID(ctx, userID)
    if err != nil {
        return fmt.Errorf("deactivating user %s: %w", userID, err)
    }
    if user.Status == StatusInactive {
        return &DomainError{
            Code:    "ALREADY_INACTIVE",
            Message: fmt.Sprintf("user %s is already inactive", userID),
        }
    }
    return s.repo.UpdateStatus(ctx, userID, StatusInactive)
}

Bereit für deine Go-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Häufige Interviewfragen zur Go-Fehlerbehandlung

Technische Interviews für Go-Positionen prüfen regelmäßig das Wissen über Fehlerbehandlung. Diese Fragen tauchen häufig in Screening-Runden auf und werden ausführlich in den Go-Interviewfragen zur Fehlerbehandlung behandelt.

Warum verwendet Go Fehlerwerte anstelle von Exceptions?

Exceptions erzeugen unsichtbaren Kontrollfluss. Eine Funktion, die wirft, überträgt die Kontrolle an eine unbekannte catch-Stelle, möglicherweise viele Stack-Frames entfernt. Go-Fehlerwerte machen jeden Fehlerpfad explizit in der Funktionssignatur sichtbar. Der Aufrufer entscheidet sofort, wie der Fehler behandelt wird, ob durch Retry, Wrapping, Logging oder Weitergabe. Dies eliminiert das Problem der überraschenden Throws und macht Fehlerabläufe ohne Tool-Unterstützung lesbar.

Was ist der Unterschied zwischen %w und %v in fmt.Errorf?

%w wrappt den Fehler und erhält die Kette für errors.Is und errors.As. %v formatiert den Fehler als String und erzeugt einen neuen Fehler ohne Kettenverbindung zum Original. %w sollte verwendet werden, wenn Aufrufer die Ursache inspizieren können sollen; %v ist angebracht, wenn die Ursache ein Implementierungsdetail ist, das nicht durch eine Abstraktionsgrenze durchdringen soll.

Wann sollte eine Funktion einen Fehler zurückgeben und wann panic aufrufen?

Panic ist für wirklich nicht behebbare Situationen reserviert: Programmierfehler wie Index außerhalb des Bereichs, Nil-Pointer-Dereferenzierungen oder verletzte Invarianten, die auf einen Bug hinweisen. Behebbare Fehler wie Netzwerk-Timeouts, fehlende Datensätze oder ungültige Eingaben geben Fehler zurück. Eine nützliche Regel: Wenn der Zustand während des normalen Betriebs auftreten kann, wird ein Fehler zurückgegeben. Wenn es bedeutet, dass das Programm einen Bug hat, wird panic ausgelöst.

Wie beeinflusst die Unwrap-Methode die Fehlerketteninspektion?

Wenn ein benutzerdefinierter Fehlertyp Unwrap() error implementiert, folgen die Funktionen errors.Is und errors.As dieser Methode, um die Kette zu durchlaufen. Ohne Unwrap endet die Kette bei diesem Fehler. Seit Go 1.20 können Fehler auch Unwrap() []error implementieren, um mehrere gewrappte Fehler zurückzugeben, was baumförmige Fehlerketten für Fälle wie das Aggregieren von Validierungsfehlern ermöglicht.

Anti-Patterns in der Fehlerbehandlung

Mehrere Anti-Patterns erscheinen häufig in Go-Codebasen und in Code-Review-Sitzungen:

Fehler stillschweigend ignorieren. Der Blank Identifier macht dies syntaktisch einfach, aber semantisch gefährlich:

go
// anti-pattern: silent error swallowing
result, _ := riskyOperation()

Jeder ignorierte Fehler sollte eine bewusste, dokumentierte Entscheidung sein, niemals eine Abkürzung.

Loggen und Zurückgeben. Dieses Muster erzeugt doppelte Log-Einträge und verwirrt den Aufrufer, der den Fehler ebenfalls behandelt:

go
// anti-pattern: double handling
if err != nil {
    log.Printf("operation failed: %v", err)
    return err
}

Die Lösung: Entweder den Fehler behandeln (loggen, Standardwert zurückgeben, wiederholen) oder weitergeben. Nicht beides.

String-Matching zur Fehlerklassifizierung. Das Prüfen der err.Error()-Ausgabe mit strings.Contains ist fragil. Fehlermeldungen sind keine API-Verträge und können sich zwischen Bibliotheksversionen ändern. errors.Is für Sentinel-Werte und errors.As für Typen verwenden.

Fehler wrappen, die Goroutine-Grenzen ohne Synchronisierung überschreiten. Wenn mehrere Goroutines gleichzeitig Fehler erzeugen, sollte errgroup.Group aus dem Paket golang.org/x/sync oder ein ähnlicher Synchronisierungsmechanismus verwendet werden.

Fehlerbehandlung mit errgroup für nebenläufige Operationen

Das errgroup-Paket aus golang.org/x/sync bietet ein sauberes Muster zum Sammeln von Fehlern aus nebenläufigen Goroutines:

batch.gogo
func (s *OrderService) ProcessBatch(ctx context.Context, orderIDs []string) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10)

    for _, id := range orderIDs {
        g.Go(func() error {
            if err := s.processOrder(ctx, id); err != nil {
                return fmt.Errorf("processing order %s: %w", id, err)
            }
            return nil
        })
    }

    return g.Wait()
}

errgroup.WithContext bricht den abgeleiteten Context ab, wenn eine Goroutine einen Fehler zurückgibt, und signalisiert den anderen, anzuhalten. Dies vermeidet verschwendete Arbeit und liefert einen einzelnen, gewrappten Fehler an den Aufrufer.

Go 1.24 und Fehlerbehandlung

Go 1.24 (Februar 2025) hat das errors-Paket nicht verändert. Die Fehlerbehandlungs-API ist seit Go 1.13 stabil, mit der Go 1.20-Erweiterung des Multi-Error-Unwrapping über Unwrap() []error. Aktuelle Vorschläge für eine verbesserte Fehlerbehandlungssyntax werden weiterhin im Go Issue Tracker diskutiert.

Fazit

  • Fehler als Werte behandeln: Sie explizit zurückgeben und an jeder Aufrufstelle verarbeiten, anstatt sich auf versteckte Exception-Mechanismen zu verlassen
  • Sentinel Errors (var ErrX = errors.New(...)) nur für Zustände verwenden, bei denen Aufrufer verzweigen müssen, und sie als öffentliche API behandeln
  • Fehler mit fmt.Errorf und %w wrappen, um Kontext hinzuzufügen und die Kette zu erhalten; %v verwenden, um die Kette an Abstraktionsgrenzen bewusst zu unterbrechen
  • errors.Is dem ==-Operator und errors.As der Type Assertion vorziehen, da beide die gesamte gewrappte Kette durchlaufen
  • Unwrap() error bei benutzerdefinierten Fehlertypen implementieren, damit die Ketteninspektion korrekt funktioniert
  • Behandeln oder Weitergeben, niemals beides. Einen Fehler zu loggen und zurückzugeben erzeugt doppeltes Rauschen
  • errgroup für nebenläufige Fehlersammlung verwenden anstelle manueller Goroutine-Synchronisierung
  • Fehlermeldungen kleingeschrieben ohne Satzzeichen halten, gemäß Go-Konventionen

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#go
#error-handling
#best-practices
#interview

Teilen

Verwandte Artikel