Drei Arten von Fehlern
Go-Code verwendet drei Formen von Fehlern, von der einfachsten zur reichhaltigsten:
- Ein Ad-hoc-Fehler:
errors.New("...")oderfmt.Errorf("..."), an Ort und Stelle erzeugt. Aufrufer können nur die Meldung lesen. - Ein Sentinel-Fehler: eine Variable auf Paketebene wie
io.EOF. Aufrufer können miterrors.Isdarauf prüfen. - Ein Fehlertyp: ein Struct mit einer Methode
Error(), das Felder trägt. Aufrufer können ihn miterrors.Asherausholen und die Felder lesen.
Nimm die einfachste Form, mit der Aufrufer tun können, was sie brauchen.
Sentinel-Fehler
Ein Sentinel ist ein Fehlerwert, der einmal deklariert und über seine Identität verglichen wird. Per Konvention beginnen die Namen mit Err.
Ausgabe:
mug bought
cap is out of stock, notify me later
buy "hat": not found
Zwei Aufrufe von errors.New mit demselben Text erzeugen verschiedene Fehler: errors.New("x") == errors.New("x") ist false. Deshalb muss ein Sentinel eine einzige geteilte Variable sein.
Sentinels werden Teil der API deines Pakets. Sobald Aufrufer auf ErrNotFound prüfen, kannst du nicht aufhören, ihn zurückzugeben, ohne sie zu brechen. Exportiere nur die, nach denen Aufrufer wirklich verzweigen müssen.
Eigene Fehlertypen
Braucht der Aufrufer Details (welches Feld, welcher Statuscode, welche Wartezeit bis zum Retry), definier einen Typ.
Ausgabe:
register: age: must be between 0 and 150
bad field: age
Die Methode hat einen Pointer Receiver, und die Funktion gibt &ValidationError{...} zurück, also ist das Ziel für errors.As ein *ValidationError, und du übergibst dessen Adresse (&ve, ein **ValidationError). Diese Ebene falsch zu treffen ist der klassische Fehler bei errors.As: Mit einem Value Receiver würdest du ValidationError{...} zurückgeben und var ve ValidationError deklarieren. go vet findet den häufigsten Ausrutscher, ve statt &ve zu übergeben: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type.
Wrappen mit %w
fmt.Errorf mit %w gibt einen Fehler zurück, der sich an den gewrappten erinnert. Jede Schicht fügt Kontext hinzu und hält die Kette intakt.
Ausgabe:
start: connect db: timeout
*fmt.wrapError: start: connect db: timeout
*fmt.wrapError: connect db: timeout
*errors.errorString: timeout
true
false
Mit %v ist die Meldung dieselbe, aber die Kette ist unterbrochen, also gibt errors.Is false zurück. Nimm %w, wenn Aufrufer die Ursache sehen sollen, und %v, wenn die Ursache ein Implementierungsdetail ist, von dem sie nicht abhängen sollen.
Seit Go 1.20 kann ein Aufruf mehrere Fehler wrappen: fmt.Errorf("%w; %w", err1, err2). errors.Is trifft dann auf jeden der beiden.
errors.Join: mehrere Fehler auf einmal
Validierung und Aufräumen erzeugen oft mehr als einen Fehler. errors.Join (Go 1.20) bündelt sie.
Ausgabe:
name: required
email: required
age: negative
---
true
true
errors.Join gibt nil zurück, wenn jedes Argument nil ist, also braucht die Funktion oben keinen Sonderfall für „keine Fehler“.
Unwrap- und Is-Methoden
errors.Is und errors.As finden gewrappte Fehler, indem sie eine Methode Unwrap aufrufen. Ein eigener Typ, der eine Ursache enthält, sollte sie herausgeben:
Ein Typ, der mehrere Fehler wrappt, implementiert stattdessen Unwrap() []error.
Ein Typ kann auch Is(target error) bool definieren, um selbst über Gleichheit zu entscheiden, etwa um jeden *HTTPError mit demselben Statuscode zu treffen. Das brauchst du selten; einen Sentinel zu wrappen und ihn aus Unwrap zurückzugeben reicht meist.
Die Falle mit dem typisierten nil
Deklarier nie eine Fehlervariable mit deinem konkreten Typ und gib sie über error zurück:
func check() error {
var err *ValidationError // nil pointer
// ... no problem found
return err // non-nil error! Its type is *ValidationError
}
Das if err != nil des Aufrufers ist wahr, weil ein Interface, das einen typisierten nil-Pointer enthält, nicht nil ist. Gib bei Erfolg ein wörtliches nil zurück und typisiere lokale Fehlervariablen als error. Die Seite zu Interfaces erklärt warum.
Welche Art du wählen solltest
| Aufrufer müssen | Biete an |
|---|---|
| den Fehlschlag nur loggen oder anzeigen | fmt.Errorf("...: %w", err) |
| nach einer bestimmten Bedingung verzweigen | einen Sentinel var ErrX = errors.New(...) |
| Details über den Fehlschlag lesen | einen Fehlertyp mit Feldern |
| mehrere unabhängige Fehlschläge sehen | errors.Join |
Häufige Fehler
- Gewrappte Fehler mit
==vergleichen. Nimmerrors.Is. - Einen Nicht-Pointer an
errors.Asübergeben. Es braucht einen Pointer auf eine Variable des Zieltyps. - Einen „Sentinel“ in einer Funktion erzeugen.
return errors.New("not found")erzeugt bei jedem Aufruf einen neuen Wert; Aufrufer können nicht dagegen vergleichen. - Jeden Fehler exportieren. Jeder exportierte Sentinel und jeder exportierte Typ ist ein API-Versprechen.
- Auf
err.Error()prüfen. Strings sind für Menschen.
Häufig gestellte Fragen
Wie erstelle ich einen eigenen Fehler in Go?
Für eine feste Bedingung deklarierst du einen Sentinel auf Paketebene: var ErrNotFound = errors.New("not found"). Für einen Fehler, der Daten trägt, definierst du einen Typ mit einer Methode Error() string: type ValidationError struct { Field string } und func (e *ValidationError) Error() string { return e.Field + " is invalid" }.
Wie wrappt man einen Fehler in Go?
Mit fmt.Errorf und dem Verb %w: return fmt.Errorf("load user %d: %w", id, err). Die Meldung des neuen Fehlers enthält die alte, und errors.Unwrap, errors.Is und errors.As erreichen das Original. Seit Go 1.20 kann ein einziger Errorf-Aufruf mit mehreren %w-Verben mehrere Fehler wrappen.
Was ist der Unterschied zwischen errors.Is und errors.As?
errors.Is(err, target) beantwortet „steckt genau dieser Fehlerwert irgendwo in der Kette“, für Sentinels wie io.EOF. errors.As(err, &target) beantwortet „gibt es einen Fehler dieses Typs in der Kette“ und speichert ihn gegebenenfalls in target, damit du seine Felder lesen kannst.
Was macht errors.Join?
errors.Join(errs...) (Go 1.20) fasst mehrere Fehler zu einem zusammen. Seine Meldung besteht aus den einzelnen Meldungen, getrennt durch Zeilenumbrüche, nil-Fehler werden weggelassen, und es gibt nil zurück, wenn alle nil sind. errors.Is und errors.As treffen, wenn irgendeiner der zusammengefassten Fehler passt.