Erstellen benutzerdefinierter Fehler in Go

Einführung

Go bietet zwei Methoden zum Erstellen von Fehlern in der Standardbibliothek:errors.New and fmt.Errorf. Bei der Übermittlung komplizierterer Fehlerinformationen an Ihre Benutzer oder an Ihr zukünftiges Selbst beim Debuggen reichen diese beiden Mechanismen manchmal nicht aus, um die Ereignisse angemessen zu erfassen und zu melden. Um diese komplexeren Fehlerinformationen zu übermitteln und mehr Funktionalität zu erreichen, können wir den Standard-Bibliotheksschnittstellentyperror implementieren.

Die Syntax dafür wäre wie folgt:

type error interface {
  Error() string
}

Das Paketbuiltin definierterror als Schnittstelle mit einer einzelnenError()-Methode, die eine Fehlermeldung als Zeichenfolge zurückgibt. Durch die Implementierung dieser Methode können wir jeden von uns definierten Typ in einen eigenen Fehler umwandeln.

Versuchen wir, das folgende Beispiel auszuführen, um eine Implementierung dererror-Schnittstelle zu sehen:

package main

import (
    "fmt"
    "os"
)

type MyError struct{}

func (m *MyError) Error() string {
    return "boom"
}

func sayHello() (string, error) {
    return "", &MyError{}
}

func main() {
    s, err := sayHello()
    if err != nil {
        fmt.Println("unexpected error: err:", err)
        os.Exit(1)
    }
    fmt.Println("The string:", s)
}

Wir werden die folgende Ausgabe sehen:

Outputunexpected error: err: boom
exit status 1

Hier haben wir einen neuen leeren Strukturtyp,MyError, erstellt und dieError()-Methode darauf definiert. Die MethodeError() gibt die Zeichenfolge"boom" zurück.

Innerhalb vonmain() rufen wir die FunktionsayHello auf, die eine leere Zeichenfolge und eine neue Instanz vonMyError zurückgibt. DasayHello immer einen Fehler zurückgibt, wird der Aufruf vonfmt.Println im Hauptteil der if-Anweisung inmain() immer ausgeführt. Wir verwenden dannfmt.Println, um die kurze Präfixzeichenfolge"unexpected error:" zusammen mit der Instanz vonMyError zu drucken, die in der Variablenerr enthalten ist.

Beachten Sie, dass wirError() nicht direkt aufrufen müssen, da das Paketfmt automatisch erkennen kann, dass es sich um eine Implementierung vonerror handelt. Es ruftError()transparently auf, um die Zeichenfolge"boom" abzurufen, und verkettet sie mit der Präfixzeichenfolge"unexpected error: err:".

Sammeln detaillierter Informationen in einem benutzerdefinierten Fehler

Manchmal ist ein benutzerdefinierter Fehler die sauberste Methode, um detaillierte Fehlerinformationen zu erfassen. Angenommen, wir möchten den Statuscode für Fehler erfassen, die durch eine HTTP-Anforderung verursacht wurden. Führen Sie das folgende Programm aus, um eine Implementierung vonerror anzuzeigen, mit der wir diese Informationen sauber erfassen können:

package main

import (
    "errors"
    "fmt"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("success!")
}

Wir werden die folgende Ausgabe sehen:

Outputstatus 503: err unavailable
exit status 1

In diesem Beispiel erstellen wir eine neue Instanz vonRequestError und geben den Statuscode und einen Fehler mithilfe der Funktionerrors.New aus der Standardbibliothek an. Wir drucken dies dann mitfmt.Println wie in den vorherigen Beispielen.

Innerhalb derError()-Methode vonRequestError verwenden wir diefmt.Sprintf-Funktion, um eine Zeichenfolge unter Verwendung der Informationen zu erstellen, die bei der Erstellung des Fehlers angegeben wurden.

Typzusicherungen und benutzerdefinierte Fehler

Dieerror-Schnittstelle macht nur eine Methode verfügbar, aber wir müssen möglicherweise auf die anderen Methoden dererror-Implementierungen zugreifen, um einen Fehler ordnungsgemäß zu behandeln. Beispielsweise haben wir möglicherweise mehrere benutzerdefinierte Implementierungen vonerror, die temporär sind und wiederholt werden können - gekennzeichnet durch das Vorhandensein einerTemporary()-Methode.

Schnittstellen bieten einen engen Überblick über die breiteren Methoden, die von Typen bereitgestellt werden. Daher müssen wirtype assertion verwenden, um die angezeigten Methoden zu ändern oder sie vollständig zu entfernen.

Das folgende Beispiel erweitert die zuvor gezeigtenRequestError um eineTemporary()-Methode, die angibt, ob Anrufer die Anforderung wiederholen sollten oder nicht:

package main

import (
    "errors"
    "fmt"
    "net/http"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return r.Err.Error()
}

func (r *RequestError) Temporary() bool {
    return r.StatusCode == http.StatusServiceUnavailable // 503
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        re, ok := err.(*RequestError)
        if ok {
            if re.Temporary() {
                fmt.Println("This request can be tried again")
            } else {
                fmt.Println("This request cannot be tried again")
            }
        }
        os.Exit(1)
    }

    fmt.Println("success!")
}

Wir werden die folgende Ausgabe sehen:

Outputunavailable
This request can be tried again
exit status 1

Innerhalb vonmain() rufen wirdoRequest() auf, was uns eineerror-Schnittstelle zurückgibt. Wir drucken zuerst die Fehlermeldung, die von derError()-Methode zurückgegeben wird. Als nächstes versuchen wir, alle Methoden ausRequestError unter Verwendung der Typzusicherungre, ok := err.(*RequestError) verfügbar zu machen. Wenn die Typzusicherung erfolgreich war, verwenden wir die MethodeTemporary(), um festzustellen, ob dieser Fehler ein vorübergehender Fehler ist. Da die durchdoRequest() festgelegtenStatusCode503 sind, washttp.StatusServiceUnavailable entspricht, gibt diestrue zurück und bewirkt, dass"This request can be tried again" gedruckt werden. In der Praxis würden wir stattdessen eine andere Anfrage stellen, anstatt eine Nachricht zu drucken.

Verpackungsfehler

In der Regel wird ein Fehler außerhalb Ihres Programms generiert, z. B .: eine Datenbank, eine Netzwerkverbindung usw. Die Fehlermeldungen dieser Fehler helfen niemandem, die Fehlerursache zu finden. Das Umschließen von Fehlern mit zusätzlichen Informationen am Anfang einer Fehlermeldung bietet den erforderlichen Kontext für ein erfolgreiches Debuggen.

Das folgende Beispiel zeigt, wie wir einige Kontextinformationen an ein ansonsten kryptischeserroranhängen können, das von einer anderen Funktion zurückgegeben wird:

package main

import (
    "errors"
    "fmt"
)

type WrappedError struct {
    Context string
    Err     error
}

func (w *WrappedError) Error() string {
    return fmt.Sprintf("%s: %v", w.Context, w.Err)
}

func Wrap(err error, info string) *WrappedError {
    return &WrappedError{
        Context: info,
        Err:     err,
    }
}

func main() {
    err := errors.New("boom!")
    err = Wrap(err, "main")

    fmt.Println(err)
}

Wir werden die folgende Ausgabe sehen:

Outputmain: boom!

WrappedError ist eine Struktur mit zwei Feldern: eine Kontextnachricht alsstring und eineerror, über die diesesWrappedError weitere Informationen liefert. Wenn die MethodeError() aufgerufen wird, verwenden wir erneutfmt.Sprintf, um die Kontextnachricht zu drucken. Dann wissen dieerror (fmt.Sprintf, dass sie die MethodeError() implizit als aufrufen Gut).

Innerhalb vonmain() erstellen wir einen Fehler miterrors.New und schließen diesen Fehler mit der von uns definierten FunktionWrap ein. Dies ermöglicht es uns anzuzeigen, dass dieseerror in"main" erzeugt wurden. Da unserWrappedError auch einerror ist, könnten wir auch andere `WrappedError`s einschließen - dies würde es uns ermöglichen, eine Kette zu sehen, die uns hilft, die Fehlerquelle aufzuspüren. Mit ein wenig Hilfe aus der Standardbibliothek können wir sogar komplette Stack-Traces in unsere Fehler einbetten.

Fazit

Da dieerror-Schnittstelle nur eine einzige Methode ist, haben wir festgestellt, dass wir sehr flexibel verschiedene Arten von Fehlern für verschiedene Situationen bereitstellen können. Dies kann alles umfassen, von der Übermittlung mehrerer Informationen als Teil eines Fehlers bis hin zur Implementierung vonexponential backoff. Während die Fehlerbehandlungsmechanismen in Go auf den ersten Blick simpel erscheinen, können wir mit diesen benutzerdefinierten Fehlern eine umfassende Behandlung erzielen, um sowohl häufige als auch ungewöhnliche Situationen zu behandeln.

Go hat einen anderen Mechanismus, um unerwartetes Verhalten zu kommunizieren: Panik. In unserem nächsten Artikel in der Fehlerbehandlungsserie werden wir Paniken untersuchen - was sie sind und wie sie behandelt werden.