Menu

Golang Fehlerbehandlung: if err != nil, Wrapping, Prüfen

Go behandelt Fehler als gewöhnliche Werte, die Funktionen zurückgeben. Das Interface error, if err != nil, errors.New und fmt.Errorf, Fehler mit Kontext zurückgeben, sie mit errors.Is und errors.As prüfen und jeden Fehler nur einmal behandeln.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Fehler sind Werte

Eine Go-Funktion, die fehlschlagen kann, gibt als letztes Ergebnis einen error zurück. Der Aufrufer prüft ihn sofort.

Ausgabe:

parsed: 42
could not parse: strconv.Atoi: parsing "forty-two": invalid syntax

Das ist der ganze Mechanismus. Es gibt keine Exceptions, kein try oder catch und keinen versteckten Kontrollfluss: Ein Fehler wandert nur dorthin, wohin dein Code ihn weitergibt. Der Preis ist die sichtbare Wiederholung von if err != nil. Der Gewinn: Jede Fehlerstelle ist auf der Seite sichtbar, und du entscheidest an jeder, was passiert.

Der Typ error

error ist ein eingebautes Interface mit einer Methode:

type error interface {
	Error() string
}

Jeder Typ mit einer Methode Error() string ist ein Fehler. Ein nil-Fehler bedeutet Erfolg. Gibst du einen Fehler mit fmt.Println(err) oder %v aus, wird Error() aufgerufen.

Fehler erzeugen

Zwei Funktionen decken die meisten Fälle ab.

errors.New erzeugt einen Fehler mit festem Text. fmt.Errorf formatiert einen, mit denselben Verben wie Printf. Fehlermeldungen beginnen per Konvention klein und haben kein Satzzeichen am Ende, weil sie meist in längere Meldungen eingebettet werden: load config: open app.yaml: no such file or directory.

Das Muster if err != nil

Die idiomatische Form: aufrufen, prüfen, früh zurückkehren. Der Erfolgspfad bleibt am linken Rand, und jeder Fehlschlag steigt aus, sobald er passiert.

func loadUser(id int) (*User, error) {
	row, err := db.Query(id)
	if err != nil {
		return nil, err
	}
	u, err := parseUser(row)
	if err != nil {
		return nil, err
	}
	if err := u.Validate(); err != nil {
		return nil, err
	}
	return u, nil
}

Zwei Konventionen, die dir auffallen sollten:

  • Bei einem Fehler gibst du für die anderen Ergebnisse den Nullwert zurück (nil, 0, ""). Aufrufer dürfen sie nicht verwenden, wenn err != nil gilt.
  • if err := f(); err != nil beschränkt den Scope von err auf das if, wenn die Funktion nur einen Fehler zurückgibt. Das hält den äußeren Scope sauber.

Vermeide das else nach einem Fehler-Return. if err != nil { return err } else { ... } rückt nur den Happy Path grundlos ein.

Kontext hinzufügen, wenn du einen Fehler zurückgibst

Ein unverändert weitergereichter Fehler verliert die Geschichte seiner Herkunft. open config.yaml: no such file or directory sagt dir nicht, welcher Schritt beim Start fehlgeschlagen ist. Füg mit fmt.Errorf und dem Verb %w Kontext hinzu:

Ausgabe:

start server: read config: open /etc/myapp/config.yaml: no such file or directory
true

Jede Schicht fügt hinzu, was sie gerade getan hat, und die endgültige Meldung liest sich wie eine Spur von der Spitze des Aufrufs bis zur Ursache. Guter Kontext nennt die Operation und die Eingabe: parse line 12, fetch user 42. Häng nicht auf jeder Ebene „error“ oder „failed“ an; die Meldung ist bereits ein Fehler.

%w wrappt: Es bewahrt den ursprünglichen Fehler im neuen auf, sodass errors.Is und errors.As ihn weiterhin finden. %v kopiert nur den Text. Nimm %v, wenn du ein Implementierungsdetail bewusst vor Aufrufern verbergen willst, zum Beispiel damit sie nicht vom Fehlertyp eines Datenbanktreibers abhängig werden.

Auf bestimmte Fehler prüfen: errors.Is und errors.As

Manchmal muss der Aufrufer auf eine bestimmte Art von Fehlschlag reagieren: Eine fehlende Datei heißt „Standardwerte nehmen“, ein Timeout heißt „nochmal versuchen“. Zwei Funktionen beantworten das, und beide schauen durch jede Schicht des Wrappings.

Faustregeln:

  • Vergleiche mit vordefinierten Fehlerwerten (Sentinels wie io.EOF, os.ErrNotExist, sql.ErrNoRows) per errors.Is, nicht per ==. == scheitert, sobald der Fehler gewrappt ist.
  • Hol einen typisierten Fehler aus demselben Grund mit errors.As heraus, nicht mit einer Type Assertion. errors.As nimmt einen Pointer auf eine Variable des Zieltyps.
  • Prüf nie auf den Text von err.Error(). Meldungen ändern sich zwischen Versionen, und Textvergleiche brechen dann still.

Eigene Sentinel-Fehler und Fehlertypen zu definieren und mehrere Fehler mit errors.Join zusammenzufassen, behandelt die Seite zu eigenen Fehlern.

Einen Fehler nur einmal behandeln

Ein Fehler sollte genau einmal behandelt werden. Behandeln heißt eins davon: ihn zurückgeben (meist gewrappt), ihn loggen und weitermachen, es erneut versuchen oder ihn in eine Antwort für den Nutzer umwandeln. Zwei davon zu tun ist der häufigste Fehler-Bug in Go-Code.

// Wrong: logged here, and returned, so it is logged again by every caller.
if err != nil {
	log.Printf("could not fetch user: %v", err)
	return err
}

// Right: add context and return. The top of the program logs once.
if err != nil {
	return fmt.Errorf("fetch user %d: %w", id, err)
}

Loggen und Zurückgeben erzeugt denselben Fehlschlag mehrfach in den Logs, jedes Mal mit weniger Kontext als die endgültige Meldung. Lass Fehler nach oben fließen bis zu der Stelle, die entscheiden kann, was zu tun ist (ein HTTP-Handler, ein main, eine Worker-Schleife), und logge dort.

Wo Fehler landen

An der Spitze des Programms muss etwas auf den Fehler reagieren. In main heißt das meist, ihn auszugeben und mit einem Status ungleich null zu beenden:

Ohne Argumente gibt das error: usage: app <name> auf stderr aus und beendet sich mit Status 1 (trag im Args-Panel einen Namen ein, um den anderen Pfad zu sehen). Hält sich main an diese Form und steckt die eigentliche Arbeit in run, ist das Programm testbar, und defer-Anweisungen in run laufen weiterhin, da os.Exit per defer eingeplante Aufrufe überspringt.

In einem HTTP-Server ist die Spitze der Handler: Er bildet den Fehler auf einen Statuscode und eine sichere Meldung für den Client ab und loggt die ausführliche Meldung für dich.

Fehler, die du ignorieren darfst, und solche, die du nicht ignorieren darfst

Einen Fehler zu ignorieren ist manchmal richtig, aber mach es mit _ explizit, damit Leser wissen, dass es eine Entscheidung war:

_ = conn.SetDeadline(t) // best effort

Manche Aufrufe können in der Praxis nicht fehlschlagen (strings.Builder.WriteString, bytes.Buffer.Write). Andere wirken harmlos und sind es nicht: Close auf einer Datei, die du geschrieben hast, kann melden, dass die Daten nie auf der Platte angekommen sind, und json.Marshal scheitert an Channels und Funktionen. Im Zweifel prüfen.

Der Linter errcheck (enthalten in golangci-lint) meldet ungeprüfte Fehler. go vet allein markiert sie nicht.

Fehler und Panics

Go hat auch panic, aber das ist kein Exception-System. Nimm Fehler für alles, was im normalen Betrieb schiefgehen kann: falsche Eingaben, fehlende Dateien, Netzwerkfehler. Heb Panic für Bugs auf (ein unmöglicher Zustand, eine verletzte Invariante) und für Fehler beim Start, bei denen Weitermachen keinen Sinn ergibt. Eine Bibliothek sollte fast nie eine Panic über ihre API hinaus auslösen. Siehe panic und recover.

Weniger Wiederholung

if err != nil ist wortreich, und Vorschläge für neue Syntax dafür wurden wiederholt abgelehnt; das Go-Team hat 2025 angekündigt, Syntaxänderungen für die Fehlerbehandlung nicht weiter zu verfolgen. Innerhalb der Sprache reduzieren einige Muster das Rauschen:

  • Früh zurückkehren und Funktionen klein halten. Die meiste Wiederholung kommt von langen Funktionen mit vielen Schritten.
  • Der klebrige Fehler. Bei einer Folge von Schreibzugriffen behältst du den ersten Fehler in einem Struct-Feld und machst spätere Aufrufe wirkungslos, sobald er gesetzt ist. bufio.Writer arbeitet so: Du prüfst den Fehler einmal nach Flush.
  • Einmal pro Funktion wrappen. Eine per defer eingeplante Closure über ein benanntes Ergebnis kann jedem Fehler, den die Funktion zurückgibt, denselben Kontext hinzufügen (siehe defer).

Häufige Fehler

  • Einen Wert verwenden, obwohl err nicht nil ist. Erst prüfen, dann verwenden.
  • Loggen und zurückgeben. Entscheide dich für eins.
  • Nach dem Wrappen mit == vergleichen. Nimm errors.Is.
  • Die Ursache mit %v verlieren. Nimm %w, außer das Verbergen ist der Zweck.
  • Einen typisierten nil-Pointer als error zurückgeben. var e *MyErr; return e ist für den Aufrufer nicht nil. Gib ein wörtliches nil zurück.
  • Meldungen mit Großbuchstaben oder Satzzeichen. errors.New("Failed to connect.") liest sich gewrappt schlecht. Schreib connect to db: ....

Häufig gestellte Fragen

Wie funktioniert die Fehlerbehandlung in Go?

Funktionen, die fehlschlagen können, geben als letztes Ergebnis einen error zurück. Der Aufrufer prüft ihn sofort: v, err := f(); if err != nil { return err }. Ein error ist ein gewöhnlicher Interface-Wert mit einer Methode, Error() string, und nil bedeutet Erfolg. Exceptions gibt es nicht.

Hat Go try/catch?

Nein. Go hat keine Exceptions und kein try/catch. Erwartbare Fehlschläge werden als error-Werte zurückgegeben und mit if err != nil geprüft. panic und recover gibt es, aber sie sind für Programmierfehler und nicht behebbare Zustände gedacht, nicht für den normalen Fehlerfluss.

Wie gebe ich in Go einen Fehler zurück?

Deklarier error als letztes Ergebnis und gib bei Erfolg nil zurück. Erzeuge Fehler mit errors.New("message") für festen Text oder mit fmt.Errorf("reading %s: %w", name, err), um einem erhaltenen Fehler Kontext hinzuzufügen. Bei einem Fehlschlag gibst du für die anderen Ergebnisse Nullwerte zurück.

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

Beide übernehmen die Meldung des ursprünglichen Fehlers in den neuen. %w wrappt ihn zusätzlich, sodass errors.Is und errors.As das Original weiterhin finden. %v erzeugt einen neuen Fehler nur mit dem Text. Nimm %w, wenn Aufrufer die Ursache prüfen müssen könnten, und %v, wenn du sie verbergen willst.

Wie prüfe ich in Go, welcher Fehler zurückgegeben wurde?

Mit errors.Is(err, target) vergleichst du mit einem Sentinel-Fehler wie io.EOF oder os.ErrNotExist, und mit errors.As(err, &target) holst du einen bestimmten Fehlertyp wie *fs.PathError heraus. Beide laufen durch gewrappte Fehler. Vermeide es, Strings aus err.Error() zu vergleichen.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S