Fehlerbehandlung in Golang

Im Gegensatz zu konventionellen Methoden in anderen Mainstream-Programmiersprachen wie JavaScript (mit der try… catch-Anweisung) oder Python (mit dem try… except-Block) erfordert die Fehlerbehandlung in Go einen anderen Ansatz. Warum? Weil die Funktionen zur Fehlerbehandlung oft falsch angewandt werden.

In diesem Blogbeitrag werfen wir einen Blick auf die besten Praktiken, die zur Behandlung von Fehlern in einer Go-Anwendung verwendet werden können. Ein grundlegendes Verständnis der Funktionsweise von Go ist alles, was erforderlich ist, um diesen Artikel zu verstehen – sollten Sie an einem bestimmten Punkt nicht weiterkommen, ist es in Ordnung, sich etwas Zeit zu nehmen und unbekannte Konzepte zu recherchieren.

Der Blank-Bezeichner

Der Blank-Bezeichner ist ein anonymer Platzhalter. Er kann wie jeder andere Bezeichner in einer Deklaration verwendet werden, aber er führt keine Bindung ein. Der leere Bezeichner bietet eine Möglichkeit, linkshändige Werte in einer Zuweisung zu ignorieren und Compilerfehler über unbenutzte Importe und Variablen in einem Programm zu vermeiden. Die Praxis, Fehler dem leeren Bezeichner zuzuweisen, anstatt sie ordnungsgemäß zu behandeln, ist unsicher, da dies bedeutet, dass Sie sich entschieden haben, den Wert der definierten Funktion explizit zu ignorieren.

result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}

Ihr Grund dafür ist wahrscheinlich, dass Sie keinen Fehler von der Funktion erwarten (oder welcher Fehler auch immer auftreten mag), aber dies könnte kaskadierende Effekte in Ihrem Programm erzeugen. Am besten ist es, einen Fehler zu behandeln, wann immer es möglich ist.

Fehlerbehandlung durch mehrere Rückgabewerte

Eine Möglichkeit, Fehler zu behandeln, besteht darin, die Tatsache zu nutzen, dass Funktionen in Go mehrere Rückgabewerte unterstützen. So kann man eine Fehlervariable zusammen mit dem Ergebnis der Funktion, die man definiert, übergeben:

func iterate(x, y int) (int, error) {}

Im obigen Codebeispiel müssen wir die vordefinierte Variable error zurückgeben, wenn wir glauben, dass unsere Funktion scheitern könnte. error ist ein Schnittstellentyp, der im Go-Paket built-in deklariert ist, und sein Nullwert ist nil.

type error interface { Error() string }

In der Regel bedeutet die Rückgabe eines Fehlers, dass ein Problem vorliegt, und die Rückgabe von nil bedeutet, dass keine Fehler aufgetreten sind:

result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }

Wenn also die Funktion iterate aufgerufen wird und err nicht gleich nil ist, sollte der zurückgegebene Fehler entsprechend behandelt werden – eine Option könnte sein, eine Instanz eines Wiederholungs- oder Aufräummechanismus zu erstellen. Der einzige Nachteil bei der Behandlung von Fehlern auf diese Weise ist, dass es keine Durchsetzung von Go’s Compiler gibt, Sie müssen entscheiden, wie die von Ihnen erstellte Funktion den Fehler zurückgibt. Sie können eine Fehlerstruktur definieren und sie an der Position der zurückgegebenen Werte platzieren. Eine Möglichkeit, dies zu tun, ist die Verwendung der eingebauten errorString-Struktur (Sie können diesen Code auch im Quellcode von Go finden):

package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }

Im obigen Codebeispiel bettet errorString ein string ein, das von der Methode Error zurückgegeben wird. Um einen benutzerdefinierten Fehler zu erstellen, müssen Sie Ihre Fehlerstruktur definieren und Methodensätze verwenden, um Ihrer Struktur eine Funktion zuzuordnen:

// Define an error structtype CustomError struct { msg string}// Create a function Error() string and associate it to the struct.func(error * CustomError) Error() string { return error.msg}// Then create an error object using MyError struct.func CustomErrorInstance() error { return &CustomError { "File type not supported" }}

Der neu erstellte benutzerdefinierte Fehler kann dann umstrukturiert werden, um die eingebaute error-Struktur zu verwenden:

 import "errors"func CustomeErrorInstance() error { return errors.New("File type not supported")}

Eine Einschränkung der eingebauten error-Struktur ist, dass sie keine Stack Traces enthält. Das macht es sehr schwierig, den Ort eines Fehlers zu lokalisieren. Der Fehler könnte eine Reihe von Funktionen durchlaufen, bevor er ausgedruckt wird. Um damit umzugehen, können Sie das Paket pkg/errors installieren, das grundlegende Fehlerbehandlungsprimitive wie die Aufzeichnung von Stack Traces, Fehlerumhüllung, -enthüllung und -formatierung bietet. Um dieses Paket zu installieren, führen Sie diesen Befehl in Ihrem Terminal aus:

go get github.com/pkg/errors

Wenn Sie Ihren Fehlern Stacktraces oder andere Informationen hinzufügen müssen, die die Fehlersuche erleichtern, verwenden Sie die Funktionen New oder Errorf, um Fehler bereitzustellen, die Ihren Stacktrace aufzeichnen. Errorf implementiert die fmt.Formatter-Schnittstelle, mit der Sie Ihre Fehler unter Verwendung der fmt-Paket-Runen (%s, %v, %+v usw.) formatieren können:

import( "github.com/pkg/errors" "fmt")func X() error { return errors.Errorf("Could not write to file")}func customError() { return X()}func main() { fmt.Printf("Error: %+v", customError())}

Um Stack-Traces anstelle einer einfachen Fehlermeldung auszugeben, müssen Sie %+v anstelle von %v im Formatmuster verwenden, und die Stack-Traces werden ähnlich wie im folgenden Codebeispiel aussehen:

Error: Could not write to filemain.X /Users/raphaelugwu/Go/src/golangProject/error_handling.go:7main.customError /Users/raphaelugwu/Go/src/golangProject/error_handling.go:15main.main /Users/raphaelugwu/Go/src/golangProject/error_handling.go:19runtime.main /usr/local/opt/go/libexec/src/runtime/proc.go:192runtime.goexit /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471

Defer, panic, and recover

Obwohl Go keine Ausnahmen hat, verfügt es über eine ähnliche Art von Mechanismus, der als „Defer, panic, and recover“ bekannt ist. Die Ideologie von Go ist, dass das Hinzufügen von Ausnahmen wie die try/catch/finally-Anweisung in JavaScript zu komplexem Code führen und Programmierer dazu ermutigen würde, zu viele grundlegende Fehler, wie das Nicht-Öffnen einer Datei, als außergewöhnlich zu bezeichnen. defer/panic/recover sollte nicht wie throw/catch/finally verwendet werden, sondern nur bei unerwarteten, nicht behebbaren Fehlern.

Defer ist ein Sprachmechanismus, der den Funktionsaufruf auf einen Stack legt. Jede aufgeschobene Funktion wird in umgekehrter Reihenfolge ausgeführt, wenn die Host-Funktion beendet ist, unabhängig davon, ob eine Panik aufgerufen wird oder nicht. Der Aufschiebungsmechanismus ist sehr nützlich, um Ressourcen aufzuräumen:

package mainimport ( "fmt")func A() { defer fmt.Println("Keep calm!") B()}func B() { defer fmt.Println("Else...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") D()}func D() { defer fmt.Println("If it's more than 30 degrees...")}func main() { A()}

Dies würde kompiliert als:

If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!

Panic ist eine eingebaute Funktion, die den normalen Ausführungsfluss anhält. Wenn Sie panic in Ihrem Code aufrufen, bedeutet das, dass Sie beschlossen haben, dass Ihr Aufrufer das Problem nicht lösen kann. Daher sollte panic nur in seltenen Fällen verwendet werden, in denen es für Ihren Code oder jemanden, der Ihren Code integriert, nicht sicher ist, an diesem Punkt fortzufahren. Hier ist ein Codebeispiel, das zeigt, wie panic funktioniert:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

Das obige Beispiel würde kompiliert werden als:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Then we can't save the earth!panic: Global Warming!!!goroutine 1 :main.Break() /tmp/sandbox186240156/prog.go:22 +0xe0main.C() /tmp/sandbox186240156/prog.go:18 +0xa0main.B() /tmp/sandbox186240156/prog.go:14 +0xa0main.A() /tmp/sandbox186240156/prog.go:10 +0xa0main.main() /tmp/sandbox186240156/prog.go:26 +0x20Program exited: status 2.

Wie oben gezeigt, stoppt der Ausführungsfluss, wenn panic verwendet und nicht behandelt wird, alle aufgeschobenen Funktionen werden in umgekehrter Reihenfolge ausgeführt und Stack Traces werden gedruckt.

Sie können die eingebaute Funktion recover verwenden, um panic zu behandeln und die Werte zurückzugeben, die von einem Panic Call übergeben werden. recover muss immer in einer defer-Funktion aufgerufen werden, sonst gibt sie nil zurück:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") defer func() { if x := recover(); x != nil { fmt.Printf("Panic: %+v\n", x) } }() B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

Wie im obigen Codebeispiel zu sehen ist, verhindert recover, dass der gesamte Ausführungsfluss zum Stillstand kommt, weil wir eine panic-Funktion eingefügt haben und der Compiler zurückkehren würde:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Panic: Global Warming!!!Then we can't save the earth!Program exited.

Um einen Fehler als Rückgabewert zu melden, müssen Sie die recover-Funktion in derselben Goroutine aufrufen, in der auch die panic-Funktion aufgerufen wird, eine Fehlerstruktur von der recover-Funktion abrufen und sie an eine Variable übergeben:

package mainimport ( "errors" "fmt")func saveEarth() (err error) { defer func() { if r := recover(); r != nil { err = r.(error) } }() TooLate() return}func TooLate() { A() panic(errors.New("Then there's nothing we can do"))}func A() { defer fmt.Println("If it's more than 100 degrees...")}func main() { err := saveEarth() fmt.Println(err)}

Jede zurückgestellte Funktion wird nach einem Funktionsaufruf, aber vor einer Rückgabeanweisung ausgeführt. Sie können also eine zurückgegebene Variable setzen, bevor eine Rückgabeanweisung ausgeführt wird. Das obige Codebeispiel würde wie folgt kompiliert:

If it's more than 100 degrees...Then there's nothing we can doProgram exited.

Fehlerumgehung

Früher war die Fehlerumgehung in Go nur über die Verwendung von Paketen wie pkg/errors zugänglich. Mit dem neuesten Release von Go – Version 1.13 – ist jedoch Unterstützung für Error-Wrapping vorhanden. Laut den Release Notes:

Ein Fehler e kann einen anderen Fehler w umhüllen, indem er eine Methode Unwrap bereitstellt, die w zurückgibt. Sowohl e als auch w stehen Programmen zur Verfügung, so dass e zusätzlichen Kontext zu w liefern oder ihn neu interpretieren kann, während Programme weiterhin Entscheidungen auf der Grundlage von w treffen können.

Um umhüllte Fehler zu erzeugen, hat fmt.Errorf jetzt ein %w-Verb, und für die Untersuchung und das Auspacken von Fehlern wurden dem error-Paket einige Funktionen hinzugefügt:

errors.Unwrap: Diese Funktion untersucht im Grunde die zugrundeliegenden Fehler in einem Programm und legt sie offen. Sie gibt das Ergebnis des Aufrufs der Methode Unwrap auf Err zurück. Wenn der Typ von Err eine Unwrap-Methode enthält, die einen Fehler zurückgibt. Andernfalls gibt Unwrap nil zurück.

package errorstype Wrapper interface{ Unwrap() error}

Nachfolgend finden Sie eine Beispielimplementierung der Methode Unwrap:

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

errors.Is: Mit dieser Funktion können Sie einen Fehlerwert mit dem Sentinel-Wert vergleichen. Was diese Funktion von unseren üblichen Fehlerprüfungen unterscheidet, ist, dass sie den Sentinel-Wert nicht mit einem Fehler vergleicht, sondern mit jedem Fehler in der Fehlerkette. Sie implementiert auch eine Is-Methode für einen Fehler, so dass ein Fehler sich selbst als Sentinel-Wert angeben kann, obwohl er kein Sentinel-Wert ist.

func Is(err, target error) bool

In der obigen Basisimplementierung prüft Is und meldet, ob err oder einer der errors in seiner Kette gleich dem Ziel (Sentinel-Wert) ist.

errors.As: Diese Funktion bietet eine Möglichkeit, auf einen bestimmten Fehlertyp zu casten. Sie sucht nach dem ersten Fehler in der Fehlerkette, der mit dem Sentinel-Wert übereinstimmt, und wenn er gefunden wird, setzt sie den Sentinel-Wert auf diesen Fehlerwert und gibt true zurück:

package mainimport ( "errors" "fmt" "os")func main() { if _, err := os.Open("non-existing"); err != nil { var pathError *os.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }}

Sie können diesen Code im Quellcode von Go finden.

Compilerergebnis:

Failed at path: non-existingProgram exited.

Ein Fehler stimmt mit dem Sentinel-Wert überein, wenn der konkrete Wert des Fehlers dem Wert zugewiesen werden kann, auf den der Sentinel-Wert zeigt. As löst Panik aus, wenn der Sentinel-Wert kein Nicht-Nil-Zeiger auf einen Typ ist, der Fehler implementiert, oder auf einen Schnittstellentyp. As gibt false zurück, wenn err nil ist.

Zusammenfassung

Die Go-Gemeinschaft hat in letzter Zeit beeindruckende Fortschritte bei der Unterstützung verschiedener Programmierkonzepte und der Einführung von noch prägnanteren und einfacheren Methoden zur Fehlerbehandlung gemacht. Haben Sie eine Idee, wie Sie mit Fehlern umgehen, die in Ihrem Go-Programm auftreten? Lassen Sie es mich in den Kommentaren unten wissen.

Ressourcen:
Go’s programming language specification on Type assertion
Marcel van Lohuizen’s talk at dotGo 2019 – Go 2 error values today
Go 1.13 release notes

LogRocket: Voller Einblick in Ihre Webanwendungen

LogRocket ist eine Lösung zur Überwachung von Frontend-Anwendungen, mit der Sie Probleme so wiedergeben können, als ob sie in Ihrem eigenen Browser auftreten würden. Anstatt zu raten, warum Fehler auftreten, oder Benutzer nach Screenshots und Log-Dumps zu fragen, können Sie mit LogRocket die Sitzung wiedergeben, um schnell zu verstehen, was falsch gelaufen ist. Es funktioniert perfekt mit jeder App, unabhängig vom Framework, und verfügt über Plugins, um zusätzlichen Kontext von Redux, Vuex und @ngrx/store zu protokollieren.

Zusätzlich zur Protokollierung von Redux-Aktionen und -Status zeichnet LogRocket Konsolenprotokolle, JavaScript-Fehler, Stacktraces, Netzwerkanfragen/-antworten mit Headern + Bodies, Browser-Metadaten und benutzerdefinierte Protokolle auf. Es instrumentiert auch das DOM, um das HTML und CSS auf der Seite aufzuzeichnen, um pixelgenaue Videos selbst der komplexesten Single-Page-Apps zu erstellen.

Probieren Sie es kostenlos aus.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.