Golang Fehlerbehandlungsspezifikation
Fehlerarten
Es gibt einige Optionen für die Deklaration von Fehlern. Bevor Sie die Option auswählen, die am besten zu Ihrem Anwendungsfall passt, sollten Sie Folgendes beachten:
- Muss der Aufrufer den Fehler abgleichen, um ihn zu behandeln? Wenn ja, müssen wir die
errors.Is
odererrors.As
Funktionen unterstützen, indem wir Top-Level-Fehlervariablen oder benutzerdefinierte Typen deklarieren. - Enthält die Fehlermeldung einen statischen String oder einen dynamischen String, der kontextabhängige Informationen erfordert? Für statische Strings können wir [
errors.New
](https://golang.org/pkg/errors/#New] verwenden, für letztere müssen wirfmt.Errorf
oder einen benutzerdefinierten Fehlertyp verwenden. - Werden neue Fehler, die von nachgelagerten Funktionen zurückgegeben werden, weitergeleitet? Wenn ja, beachten Sie den Abschnitt zur Fehlerumhüllung.
Fehlerabgleich | Fehlermeldung | Richtlinien |
---|---|---|
Nein | statisch | errors.New |
Nein | dynamisch | fmt.Errorf |
Ja | statisch | Top-Level- var mit errors.New |
Ja | dynamisch | benutzerdefinierter error Typ |
Verwenden Sie zum Beispiel errors.New
zur Darstellung von Fehlern mit statischen Strings. Wenn der Aufrufer diesen Fehler abgleichen und behandeln muss, exportieren Sie ihn als Variable, um das Abgleichen mit errors.Is
zu unterstützen.
Kein Fehlerabgleich
// Paket foo
func Öffnen() error {
return errors.New("konnte nicht öffnen")
}
// Paket bar
if err := foo.Öffnen(); err != nil {
// Kann den Fehler nicht behandeln.
panic("unbekannter Fehler")
}
Fehlerabgleich
// Paket foo
var ErrKonnteNichtÖffnen = errors.New("konnte nicht öffnen")
func Öffnen() error {
return ErrKonnteNichtÖffnen
}
// Paket bar
if err := foo.Öffnen(); err != nil {
if errors.Is(err, foo.ErrKonnteNichtÖffnen) {
// Fehler behandeln
} else {
panic("unbekannter Fehler")
}
}
Für Fehler mit dynamischen Strings verwenden Sie fmt.Errorf
, wenn der Aufrufer ihn nicht abgleichen muss. Wenn der Aufrufer ihn tatsächlich abgleichen muss, verwenden Sie einen benutzerdefinierten error
.
Kein Fehlerabgleich
// Paket foo
func Öffnen(datei string) error {
return fmt.Errorf("Datei %q nicht gefunden", datei)
}
// Paket bar
if err := foo.Öffnen("testdatei.txt"); err != nil {
// Kann den Fehler nicht behandeln.
panic("unbekannter Fehler")
}
Fehlerabgleich
// Paket foo
type NichtGefundenFehler struct {
Datei string
}
func (e *NichtGefundenFehler) Error() string {
return fmt.Sprintf("Datei %q nicht gefunden", e.Datei)
}
func Öffnen(datei string) error {
return &NichtGefundenFehler{Datei: datei}
}
// Paket bar
if err := foo.Öffnen("testdatei.txt"); err != nil {
var nichtGefunden *NichtGefundenFehler
if errors.As(err, &nichtGefunden) {
// Fehler behandeln
} else {
panic("unbekannter Fehler")
}
}
Beachten Sie, dass wenn Sie Fehlervariablen oder -typen aus einem Paket exportieren, sie Teil des öffentlichen API des Pakets werden.
Fehlerbehandlung
Wenn ein Fehler auftritt, während eine andere Methode aufgerufen wird, gibt es normalerweise drei Möglichkeiten, damit umzugehen:
- Den ursprünglichen Fehler so zurückgeben, wie er ist.
-
fmt.Errorf
mit%w
verwenden, um dem Fehler Kontext hinzuzufügen, und ihn dann zurückgeben. -
fmt.Errorf
mit%v
verwenden, um dem Fehler Kontext hinzuzufügen, und ihn dann zurückgeben.
Wenn kein zusätzlicher Kontext hinzugefügt werden muss, den ursprünglichen Fehler so zurückgeben, wie er ist. Dies erhält den ursprünglichen Fehlerstyp und die Fehlermeldung. Dies ist besonders geeignet, wenn die zugrunde liegende Fehlermeldung genügend Informationen enthält, um zu verfolgen, wo der Fehler herkommt.
Fügen Sie ansonsten so viel Kontext wie möglich zur Fehlermeldung hinzu, damit keine mehrdeutigen Fehler wie "Verbindung abgelehnt" auftreten. Stattdessen erhalten Sie nützlichere Fehlermeldungen, wie z. B. "Serviceaufruf foo: Verbindung abgelehnt".
Verwenden Sie fmt.Errorf
, um Ihrem Fehler Kontext hinzuzufügen, und wählen Sie zwischen %w
oder %v
Verben, je nachdem, ob der Aufrufer den Grundfehler ermitteln und extrahieren können soll.
- Verwenden Sie
%w
, wenn der Aufrufer auf den zugrunde liegenden Fehler zugreifen soll. Dies ist standardmäßig für die meisten fehlerhaften Antworten geeignet, aber beachten Sie, dass der Aufrufer beginnen könnte, sich auf dieses Verhalten zu verlassen. Daher sollten bei fehlerhaften Antworten, die bekannte Variablen oder Typen sind, diese als Teil des Funktionsvertrags erfasst und getestet werden. - Verwenden Sie
%v
, um den zugrunde liegenden Fehler zu verdecken. Der Aufrufer wird ihn nicht ermitteln können, aber Sie können in Zukunft bei Bedarf zu%w
wechseln.
Bei Hinzufügen des Kontexts zum zurückgegebenen Fehler vermeiden Sie die Verwendung von Phrasen wie "Fehler beim" um den Kontext prägnant zu halten. Wenn der Fehler durch den Stapel dringt, wird er schichtweise gestapelt:
Nicht empfohlen:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"Fehler beim Erstellen des neuen Stores: %w", err)
}
// Fehler beim x: Fehler beim y: Fehler beim Erstellen des neuen Stores: the error
Empfohlen:
s, err := store.New()
if err != nil {
return fmt.Errorf(
"neuer Store: %w", err)
}
// x: y: neuer Store: the error
Sobald der Fehler jedoch an ein anderes System gesendet wird, sollte klar sein, dass die Nachricht ein Fehler ist (z. B. ein "err" Tag oder ein "Fehlerhafter" Präfix in den Protokollen).
Falsche Namensgebung
Bei als globale Variablen gespeicherten Fehlerwerten verwenden Sie das Präfix Err
oder err
, abhängig davon, ob sie exportiert werden. Bitte beachten Sie die Richtlinien. Verwenden Sie für nicht exportierte Top-Level-Konstanten und Variablen einen Unterstrich (_).
var (
// Exportieren Sie die folgenden beiden Fehler, damit Benutzer dieses Pakets sie mit errors.Is abgleichen können.
ErrBrokenLink = errors.New("Link ist defekt")
ErrCouldNotOpen = errors.New("Konnte nicht öffnen")
// Dieser Fehler wird nicht exportiert, da wir ihn nicht Teil unserer öffentlichen API machen möchten. Innerhalb des Pakets können wir ihn dennoch mit errors verwenden.
errNotFound = errors.New("nicht gefunden")
)
Für benutzerdefinierte Fehlertypen verwenden Sie das Suffix Error
.
// Ebenso wird dieser Fehler exportiert, damit Benutzer dieses Pakets ihn mit errors.As abgleichen können.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("Datei %q nicht gefunden", e.File)
}
// Dieser Fehler wird nicht exportiert, da wir ihn nicht Teil der öffentlichen API machen möchten. Innerhalb eines Pakets können wir ihn dennoch mit errors.As verwenden.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("Auflösen von %q", e.Path)
}
Umgang mit Fehlern
Wenn der Aufrufer von dem Aufrufende einen Fehler erhält, kann er den Fehler auf verschiedene Arten behandeln, basierend auf dem Verständnis des Fehlers.
Dazu gehört, ist aber nicht darauf beschränkt:
- Der Fehler wird mit
errors.Is
odererrors.As
abgeglichen, wenn das Aufrufende eine spezifische Fehlerdefinition vereinbart hat, und die Verzweigung auf unterschiedliche Weise behandelt wird. - Protokollierung des Fehlers und gnädiges Abwerten, wenn der Fehler wiederherstellbar ist.
- Rückgabe eines klar definierten Fehlers, wenn er eine domänenspezifische Fehlerbedingung darstellt.
- Rückgabe des Fehlers, ob er nun eingewickelt ist oder nicht.
Unabhängig davon, wie der Aufrufer den Fehler behandelt, sollte er in der Regel jeden Fehler nur einmal behandeln. Zum Beispiel sollte der Aufrufer den Fehler nicht protokollieren und dann zurückgeben, da sein Aufrufer den Fehler ebenfalls behandeln könnte.
Beispielsweise betrachten Sie die folgenden Szenarien:
Schlecht: Protokollierung des Fehlers und Rückgabe
Andere Aufrufer weiter oben im Stapel könnten ähnliche Aktionen für diesen Fehler ausführen. Dies würde viel Lärm in den Anwendungsprotokollen verursachen, mit wenig Nutzen.
u, err := getUser(id)
if err != nil {
// SCHLECHT: Siehe Beschreibung
log.Printf("Konnte Benutzer %q nicht abrufen: %v", id, err)
return err
}
Gut: Einwickeln des Fehlers und Rückgabe
Die Fehler weiter oben im Stapel werden diesen Fehler behandeln. Die Verwendung von %w
stellt sicher, dass sie den Fehler mit errors.Is
oder errors.As
abgleichen können, wenn relevant.
u, err := getUser(id)
if err != nil {
return fmt.Errorf("Benutzer %q abrufen: %w", id, err)
}
Gut: Protokollierung des Fehlers und gnädiges Abwerten
Wenn die Operation nicht unbedingt notwendig ist, können wir gnädiges Abwerten bieten, indem wir uns davon erholen, ohne das Erlebnis zu unterbrechen.
if err := emitMetrics(); err != nil {
// Das Fehlschlagen des Schreibens von Metriken sollte die Anwendung nicht
// unterbrechen.
log.Printf("Konnte Metriken nicht übermitteln: %v", err)
}
Gut: Abgleichen des Fehlers und angemessenes gnädiges Abwerten
Wenn das Aufrufende einen spezifischen Fehler in seiner Vereinbarung definiert hat und das Scheitern wiederherstellbar ist, den Fall dieses Fehlers abgleichen und gnädig abwerten. Für alle anderen Fälle den Fehler einwickeln und zurückgeben. Die Fehler weiter oben im Stapel werden andere Fehler behandeln.
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// Benutzer existiert nicht. Verwenden Sie UTC.
tz = time.UTC
} else {
return fmt.Errorf("Benutzer %q abrufen: %w", id, err)
}
}
Umgang mit Assertionsfehlern
Typ-Assertionen werden im Falle einer falschen Typerkennung mit einem einzelnen Rückgabewert in einer Panik enden. Verwenden Sie daher immer das "Komma, ok"-Idiom.
Nicht empfohlen:
t := i.(string)
Empfohlen:
t, ok := i.(string)
if !ok {
// Den Fehler gnädig behandeln
}
Vermeiden Sie die Verwendung von Panic
Code, der in der Produktionsumgebung ausgeführt wird, darf keinen Panic auslösen. Panic ist die Hauptursache für Kaskadenfehler. Tritt ein Fehler auf, muss die Funktion den Fehler zurückgeben und dem Aufrufer die Entscheidung überlassen, wie damit umgegangen werden soll.
Nicht empfohlen:
func run(args []string) {
if len(args) == 0 {
panic("ein Argument ist erforderlich")
}
// ...
}
func main() {
run(os.Args[1:])
}
Empfohlen:
func run(args []string) error {
if len(args) == 0 {
return errors.New("ein Argument ist erforderlich")
}
// ...
return nil
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Panic/Recover ist keine Fehlerbehandlungsstrategie. Es soll nur dann zu einer Panic kommen, wenn ein nicht wiederherstellbares Ereignis (z. B. nil-Referenz) auftritt. Eine Ausnahme bildet die Programminitialisierung: Situationen, die dazu führen würden, dass ein Programm in Panic gerät, sollten während des Programmstarts behandelt werden.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
Auch im Testcode ist es bevorzugt, t.Fatal
oder t.FailNow
anstelle von Panic zu verwenden, um sicherzustellen, dass Fehler markiert werden.
Nicht empfohlen:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
panic("Fehler bei der Testeinrichtung")
}
Empfohlen:
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal("Fehler bei der Testeinrichtung")
}