Fehlerbehandlung
In Go gibt es drei Ebenen von Ausnahmen:
error: Ein Fehler im normalen Ablauf, der behandelt werden muss. Wenn er ignoriert wird, stürzt das Programm nicht ab.panic: Ein schwerwiegendes Problem, das Programm sollte sich nach der Behandlung sofort beenden.fatal: Ein sehr kritisches Problem, das Programm sollte sich sofort beenden.
Genau genommen hat Go keine Ausnahmen, sondern drückt sie durch Fehler aus. Ebenso gibt es in Go keine try-catch-finally-Konstrukte. Der Gründer von Go hoffte, Fehler kontrollierbar zu machen, und wollte nicht, dass für jede Operation viele try-catch-Blöcke verschachtelt werden müssen. Daher werden Fehler in den meisten Fällen als Rückgabewerte von Funktionen zurückgegeben, wie im folgenden Codebeispiel:
func main() {
// Eine Datei öffnen
if file, err := os.Open("README.txt"); err != nil {
fmt.Println(err)
return
}
fmt.Println(file.Name())
}Die Absicht dieses Codes ist offensichtlich: Öffnen einer Datei namens README.txt. Wenn das Öffnen fehlschlägt, gibt die Funktion einen Fehler zurück, der ausgegeben wird. Wenn der Fehler nil ist, war das Öffnen erfolgreich und der Dateiname wird ausgegeben.
Es scheint tatsächlich einfacher als try-catch zu sein. Wenn jedoch sehr viele Funktionsaufrufe vorhanden sind, wird der Code überall mit if err != nil-Prüfungen durchsetzt sein, wie im folgenden Beispiel, das eine Demo zur Berechnung des Hash-Werts einer Datei zeigt. In diesem kurzen Codeabschnitt erscheint dreimal if err != nil.
func main() {
sum, err := checksum("main.go")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(sum)
}
func checksum(path string) (string, error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
_, err = io.Copy(hash, file)
if err != nil {
return "", err
}
var hexSum [64]byte
sum := hash.Sum(nil)
hex.Encode(hexSum[:], sum)
return string(hexSum[:]), nil
}Aus diesem Grund ist einer der am meisten kritisierten Punkte von Go die Fehlerbehandlung. Ein erheblicher Teil des Go-Quellcodes besteht aus if err != nil. Rust gibt ebenfalls Fehlerwerte zurück, aber niemand kritisiert es dafür, da es dieses Problem durch syntaktischen Zucker gelöst hat. Im Vergleich dazu bietet Go kaum syntaktischen Zucker.
Dinge sollten jedoch dialektisch betrachtet werden. Alles hat Vor- und Nachteile. Die Vorteile der Go-Fehlerbehandlung sind:
- Geringe mentale Belastung: Bei Fehlern einfach behandeln oder zurückgeben
- Lesbarkeit: Da die Behandlung sehr einfach ist, ist der Code in den meisten Fällen leicht verständlich
- Einfache Fehlersuche: Jeder Fehler wird durch den Rückgabewert eines Funktionsaufrufs erzeugt und kann Schicht für Schicht zurückverfolgt werden. Es tritt selten auf, dass plötzlich ein Fehler auftritt, ohne zu wissen, woher er kommt.
Es gibt jedoch auch viele Nachteile:
- Fehler enthalten keine Stack-Informationen (müssen durch Drittanbieter-Pakete oder eigene Kapselung gelöst werden)
- Unschön, viele sich wiederholende Codes (abhängig von persönlichen Vorlieben)
- Benutzerdefinierte Fehler werden mit
vardeklariert, es sind Variablen und keine Konstanten (tatsächlich nicht ideal) - Variable-Shadowing-Probleme
Vorschläge und Diskussionen zur Go-Fehlerbehandlung gibt es in der Community seit der Entstehung von Go. Ein Scherz besagt: Wenn Sie die Go-Fehlerbehandlung akzeptieren können, sind Sie ein qualifizierter Gopher.
TIP
Hier sind zwei Artikel des Go-Teams zur Fehlerbehandlung:
error
error ist eine normale Ablauffehler, dessen Auftreten akzeptabel ist. In den meisten Fällen sollte er behandelt werden, kann aber auch ignoriert werden. Die Schwere eines error reicht nicht aus, um das gesamte Programm zu stoppen. error ist eine vordefinierte Schnittstelle mit nur einer Methode Error(), deren Rückgabewert ein String zur Ausgabe der Fehlerinformation ist.
type error interface {
Error() string
}error wurde historisch stark überarbeitet. In Version 1.13 führte das Go-Team verkettete Fehler ein und bot einen vollständigeren Fehlerprüfmechanismus, der im Folgenden vorgestellt wird.
Erstellen
Es gibt mehrere Methoden, um einen error zu erstellen. Die erste ist die Verwendung der New-Funktion aus dem errors-Paket.
err := errors.New("dies ist ein Fehler")Die zweite ist die Verwendung der Errorf-Funktion aus dem fmt-Paket, die einen formatierten error mit Parametern ergibt.
err := fmt.Errorf("dies ist ein Fehler mit %d formatierten Parametern", 1)Hier ist ein vollständiges Beispiel:
func sumPositive(i, j int) (int, error) {
if i <= 0 || j <= 0 {
return -1, errors.New("muss eine positive ganze Zahl sein")
}
return i + j, nil
}In den meisten Fällen werden zur besseren Wartbarkeit Fehler nicht temporär erstellt, sondern häufig verwendete Fehler als globale Variablen verwendet, wie im folgenden Codeauszug aus der Datei os/errors.go:
var (
ErrInvalid = fs.ErrInvalid // "invalid argument"
ErrPermission = fs.ErrPermission // "permission denied"
ErrExist = fs.ErrExist // "file already exists"
ErrNotExist = fs.ErrNotExist // "file does not exist"
ErrClosed = fs.ErrClosed // "file already closed"
ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout"
)Wie zu sehen ist, sind sie alle mit var definierte Variablen.
Benutzerdefinierte Fehler
Durch Implementierung der Error()-Methode kann einfach ein benutzerdefinierter error erstellt werden. Zum Beispiel ist errorString im errors-Paket eine sehr einfache Implementierung.
func New(text string) error {
return &errorString{text}
}
// errorString-Struktur
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}Da die Implementierung von errorString zu einfach und nicht ausdrucksstark genug ist, wählen viele Open-Source-Bibliotheken einschließlich offizieller Bibliotheken benutzerdefinierte Fehler, um unterschiedlichen Fehleranforderungen gerecht zu werden.
Weitergabe
In einigen Fällen gibt eine aufgerufene Funktion einen Fehler zurück, aber der Aufrufer ist nicht für die Behandlung des Fehlers verantwortlich und gibt den Fehler ebenfalls als Rückgabewert zurück, an den übergeordneten Aufrufer weiter. Dieser Prozess wird als Weitergabe bezeichnet. Fehler können während der Weitergabe schichtweise verpackt werden. Wenn der übergeordnete Aufrufer den Fehlertyp bestimmen möchte, um unterschiedliche Behandlungen vorzunehmen, kann es vorkommen, dass der Fehlertyp nicht erkannt oder falsch beurteilt wird. Verkettete Fehler wurden genau eingeführt, um diese Situation zu lösen.
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}wrapError implementiert ebenfalls die error-Schnittstelle und hat eine zusätzliche Methode Unwrap, die eine Referenz auf den ursprünglichen Fehler zurückgibt. Durch schichtweises Verpacken entsteht eine Fehlerkette, entlang derer leicht der ursprüngliche Fehler gefunden werden kann. Da diese Struktur nicht extern verfügbar ist, kann sie nur mit der fmt.Errorf-Funktion erstellt werden, zum Beispiel:
err := errors.New("dies ist ein ursprünglicher Fehler")
wrapErr := fmt.Errorf("Fehler, %w", err)Bei der Verwendung muss das Formatverb %w verwendet werden, und der Parameter muss ein gültiger error sein.
Behandlung
Der letzte Schritt der Fehlerbehandlung ist die Behandlung und Prüfung von Fehlern. Das errors-Paket bietet mehrere Hilfsfunktionen zur Fehlerbehandlung.
func Unwrap(err error) errorDie errors.Unwrap()-Funktion dient zum Entpacken einer Fehlerkette. Die interne Implementierung ist einfach:
func Unwrap(err error) error {
u, ok := err.(interface { // Typ-Assertion, ob die Methode implementiert ist
Unwrap() error
})
if !ok { // Nicht implementiert, also ein basischer error
return nil
}
return u.Unwrap() // Andernfalls Unwrap aufrufen
}Nach dem Entpacken wird der von der aktuellen Fehlerkette umschlossene Fehler zurückgegeben. Der umschlossene Fehler kann immer noch eine Fehlerkette sein. Wenn der entsprechende Wert oder Typ in der Fehlerkette gefunden werden soll, kann rekursiv nach Übereinstimmungen gesucht werden. Das Standardpaket bietet jedoch bereits ähnliche Funktionen.
func Is(err, target error) boolDie errors.Is-Funktion prüft, ob eine Fehlerkette einen bestimmten Fehler enthält. Beispiel:
var originalErr = errors.New("this is an error")
func wrap1() error { // Ursprünglichen Fehler verpacken
return fmt.Errorf("wrapp error %w", wrap2())
}
func wrap2() error { // Ursprünglicher Fehler
return originalErr
}
func main() {
err := wrap1()
if errors.Is(err, originalErr) { // Wenn if err == originalErr verwendet wird, ist es false
fmt.Println("original")
}
}Daher sollte bei der Fehlerprüfung nicht der ==-Operator, sondern errors.Is() verwendet werden.
func As(err error, target any) boolDie errors.As()-Funktion sucht den ersten Typ-übereinstimmenden Fehler in der Fehlerkette und weist den Wert dem übergebenen err zu. In einigen Fällen muss der Fehler vom Typ error in einen konkreten Fehlertyp konvertiert werden, um detailliertere Fehlerinformationen zu erhalten. Eine Typ-Assertion auf einer Fehlerkette ist jedoch unwirksam, da der ursprüngliche Fehler in einer Struktur verpackt ist. Dies ist der Grund, warum die As-Funktion benötigt wird. Beispiel:
type TimeError struct { // Benutzerdefinierter error
Msg string
Time time.Time // Zeitpunkt des Fehlers
}
func (m TimeError) Error() string {
return m.Msg
}
func NewMyError(msg string) error {
return &TimeError{
Msg: msg,
Time: time.Now(),
}
}
func wrap1() error { // Ursprünglichen Fehler verpacken
return fmt.Errorf("wrapp error %w", wrap2())
}
func wrap2() error { // Ursprünglicher Fehler
return NewMyError("original error")
}
func main() {
var myerr *TimeError
err := wrap1()
// Prüfen, ob die Fehlerkette einen Fehler vom Typ *TimeError enthält
if errors.As(err, &myerr) { // Zeitpunkt von TimeError ausgeben
fmt.Println("original", myerr.Time)
}
}target muss ein Zeiger auf error sein. Da bei der Erstellung der Struktur ein Strukturzeiger zurückgegeben wird, ist error tatsächlich vom Typ *TimeError, daher muss target vom Typ **TimeError sein.
Das offizielle errors-Paket ist jedoch nicht ausreichend, da es keine Stack-Informationen bietet und keine Lokalisierung ermöglicht. Allgemein wird das offizielle Erweiterungspaket empfohlen:
github.com/pkg/errorsBeispiel:
import (
"fmt"
"github.com/pkg/errors"
)
func Do() error {
return errors.New("error")
}
func main() {
if err := Do(); err != nil {
fmt.Printf("%+v", err)
}
}Ausgabe:
some unexpected error happened
main.Do
D:/WorkSpace/Code/GoLeran/golearn/main.go:9
main.main
D:/WorkSpace/Code/GoLeran/golearn/main.go:13
runtime.main
D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/proc.go:267
runtime.goexit
D:/WorkSpace/Library/go/root/go1.21.3/src/runtime/asm_amd64.s:1650Durch formatierte Ausgabe können Stack-Informationen angezeigt werden. Standardmäßig werden keine Stacks ausgegeben. Dieses Paket ist eine erweiterte Version des Standardpakets errors. Beide wurden offiziell entwickelt, aber aus unbekannten Gründen wurde es nicht in das Standardpaket aufgenommen.
panic
panic, auf Deutsch Panik, stellt ein schwerwiegendes Programmproblem dar. Das Programm muss sofort angehalten werden, um das Problem zu behandeln, andernfalls wird es sofort beendet und Stack-Informationen werden ausgegeben. panic ist die Ausdrucksform von Laufzeitausnahmen in Go und tritt häufig bei gefährlichen Operationen auf, hauptsächlich um rechtzeitig Schäden zu begrenzen und schwerwiegendere Folgen zu vermeiden. panic führt jedoch vor dem Beenden Aufräumarbeiten durch, und panic kann auch wiederhergestellt werden, um die Programmausführung fortzusetzen.
Hier ist ein Beispiel, das Werte in eine nil-Map schreibt, was definitiv eine Panik auslöst:
func main() {
var dic map[string]int
dic["a"] = 'a'
}panic: assignment to entry in nil mapTIP
Wenn mehrere Goroutinen im Programm vorhanden sind und eine Goroutine eine panic auslöst, stürzt das gesamte Programm ab, wenn sie nicht abgefangen wird.
Erstellen
Das explizite Erstellen einer panic ist sehr einfach. Verwenden Sie die eingebaute Funktion panic. Die Funktionssignatur lautet:
func panic(v any)Die panic-Funktion akzeptiert einen Parameter v vom Typ any. Bei der Ausgabe von Fehler-Stack-Informationen wird auch v ausgegeben. Verwendungsbeispiel:
func main() {
initDataBase("", 0)
}
func initDataBase(host string, port int) {
if len(host) == 0 || port == 0 {
panic("Ungültige Datenbankverbindungsparameter")
}
// ... andere Logik
}Wenn das Initialisieren der Datenbankverbindung fehlschlägt, sollte das Programm nicht starten, da es ohne Datenbank keinen Sinn hat zu laufen. Daher sollte hier eine panic ausgelöst werden:
panic: Ungültige DatenbankverbindungsparameterAufräumarbeiten
Vor dem Beenden des Programms aufgrund einer panic werden Aufräumarbeiten durchgeführt, z. B. die Ausführung von defer-Anweisungen.
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
panic("panic")
defer fmt.Println("D")
}Ausgabe:
C
B
A
panic: panicDie defer-Anweisungen der übergeordneten Funktionen werden ebenfalls ausgeführt. Beispiel:
func main() {
defer fmt.Println("A")
defer fmt.Println("B")
fmt.Println("C")
dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panic")
defer fmt.Println(3)
}Ausgabe:
C
2
1
B
A
panic: panicdefer kann auch panic verschachteln. Hier ist ein komplexeres Beispiel:
func main() {
defer fmt.Println("A")
defer func() {
func() {
panic("panicA")
defer fmt.Println("E")
}()
}()
fmt.Println("C")
dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
}Die Ausführungsreihenfolge von verschachteltem panic in defer bleibt konsistent. Wenn panic auftritt, kann die nachfolgende Logik nicht ausgeführt werden.
C
2
1
A
panic: panicB
panic: panicAZusammenfassend gilt: Wenn panic auftritt, wird die aktuelle Funktion sofort beendet und Aufräumarbeiten wie defer werden ausgeführt. Anschließend wird es Schicht für Schicht nach oben geworfen, und übergeordnete Funktionen führen ebenfalls Aufräumarbeiten durch, bis das Programm beendet ist.
Wenn eine untergeordnete Goroutine eine panic auslöst, werden die Aufräumarbeiten der aktuellen Goroutine nicht ausgelöst. Wenn die panic bis zum Beenden der untergeordneten Goroutine nicht wiederhergestellt wird, wird das Programm direkt beendet.
var waitGroup sync.WaitGroup
func main() {
demo()
}
func demo() {
waitGroup.Add(1)
defer func() {
fmt.Println("A")
}()
fmt.Println("C")
go dangerOp()
waitGroup.Wait() // Übergeordnete Goroutine wartet blockierend auf das Ende der untergeordneten Goroutine
defer fmt.Println("D")
}
func dangerOp() {
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
waitGroup.Done()
}Ausgabe:
C
2
1
panic: panicBWie zu sehen ist, wurde keine der defer-Anweisungen in demo() ausgeführt, und das Programm wurde direkt beendet. Beachten Sie, dass ohne waitGroup zum Blockieren der übergeordneten Goroutine die Ausführungsgeschwindigkeit von demo() schneller sein kann als die Ausführungsgeschwindigkeit der untergeordneten Goroutine, was zu sehr verwirrenden Ausgabeergebnissen führen kann. Hier wird der Code leicht geändert:
func main() {
demo()
}
func demo() {
defer func() {
// Aufräumarbeiten der übergeordneten Goroutine dauern 20ms
time.Sleep(time.Millisecond * 20)
fmt.Println("A")
}()
fmt.Println("C")
go dangerOp()
defer fmt.Println("D")
}
func dangerOp() {
// Untergeordnete Goroutine führt Logik aus, dauert 1ms
time.Sleep(time.Millisecond)
defer fmt.Println(1)
defer fmt.Println(2)
panic("panicB")
defer fmt.Println(3)
}Ausgabe:
C
D
2
1
panic: panicBIn diesem Beispiel hat die übergeordnete Goroutine die Ausführung der Funktion bereits abgeschlossen und befindet sich in den Aufräumarbeiten, als die untergeordnete Goroutine eine panic auslöst. Bei der Ausführung des letzten defer tritt die panic der untergeordneten Goroutine auf, sodass das Programm direkt beendet wird.
Wiederherstellen
Wenn panic auftritt, kann die eingebaute Funktion recover() rechtzeitig behandelt werden und sicherstellen, dass das Programm weiterläuft. Sie muss in einer defer-Anweisung ausgeführt werden. Verwendungsbeispiel:
func main() {
dangerOp()
fmt.Println("Programm normal beendet")
}
func dangerOp() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic wiederhergestellt")
}
}()
panic("panic aufgetreten")
}Der Aufrufer weiß überhaupt nicht, dass innerhalb der dangerOp()-Funktion eine panic aufgetreten ist. Das Programm führt die verbleibende Logik aus und beendet sich normal. Daher lautet die Ausgabe:
panic aufgetreten
panic wiederhergestellt
Programm normal beendetTatsächlich gibt es jedoch viele implizite Fallstricke bei der Verwendung von recover(). Zum Beispiel die Verwendung von recover in einer Closure innerhalb von defer.
func main() {
dangerOp()
fmt.Println("Programm normal beendet")
}
func dangerOp() {
defer func() {
func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic wiederhergestellt")
}
}()
}()
panic("panic aufgetreten")
}Die Closure-Funktion kann als Funktionsaufruf betrachtet werden. panic wird nach oben und nicht nach unten weitergegeben, sodass die Closure-Funktion natürlich keine panic wiederherstellen kann. Daher lautet die Ausgabe:
panic: panic aufgetretenDarüber hinaus gibt es eine extreme Situation, bei der der Parameter von panic() nil ist.
func main() {
dangerOp()
fmt.Println("Programm normal beendet")
}
func dangerOp() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
fmt.Println("panic wiederhergestellt")
}
}()
panic(nil)
}In diesem Fall wird panic zwar wiederhergestellt, aber es werden keine Fehlerinformationen ausgegeben.
Ausgabe:
Programm normal beendetZusammenfassend gibt es mehrere Punkte, die bei der recover-Funktion zu beachten sind:
- Muss in
deferverwendet werden - Bei mehrfacher Verwendung kann nur eine
panicwiederhergestellt werden - Closure-
recoverstellt keinepanicder übergeordneten Funktionen wieder her - Der Parameter von
panicdarf nichtnilsein
fatal
fatal ist ein äußerst kritisches Problem. Wenn fatal auftritt, muss das Programm sofort angehalten werden, ohne Aufräumarbeiten auszuführen. In der Regel wird die Exit-Funktion aus dem os-Paket aufgerufen, um das Programm zu beenden, wie unten gezeigt:
func main() {
dangerOp("")
}
func dangerOp(str string) {
if len(str) == 0 {
fmt.Println("fatal")
os.Exit(1)
}
fmt.Println("normale Logik")
}Ausgabe:
fatalProbleme auf fatal-Ebene werden selten explizit ausgelöst. In den meisten Fällen werden sie passiv ausgelöst.
