Umgang mit Fehlern in Go 1.13


In den letzten zehn Jahren haben wir erfolgreich die Tatsache ausgenutzt, dass Go Fehler als Werte behandelt . Obwohl die Standardbibliothek nur eine minimale Unterstützung für Fehler hatte: nur die Funktionen errors.New und fmt.Errorf , die einen Fehler erzeugen, der nur eine Nachricht enthält - die integrierte Schnittstelle ermöglicht es Go-Programmierern, Informationen hinzuzufügen. Sie benötigen lediglich einen Typ, der die Error Methode implementiert:

 type QueryError struct { Query string Err error } func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() } 

Diese Arten von Fehlern treten in allen Sprachen auf und speichern eine Vielzahl von Informationen, von Zeitstempeln über Dateinamen bis hin zu Serveradressen. Fehler auf niedriger Ebene, die zusätzlichen Kontext bieten, werden häufig erwähnt.

Ein Muster, bei dem ein Fehler einen anderen enthält, ist in Go so häufig, dass nach einer heftigen Diskussion in Go 1.13 seine explizite Unterstützung hinzugefügt wurde. In diesem Artikel werden Ergänzungen zur Standardbibliothek behandelt, die die erwähnte Unterstützung bieten: drei neue Funktionen im fmt.Errorf und ein neuer Formatierungsbefehl für fmt.Errorf .

Bevor wir die Änderungen im Detail besprechen, wollen wir uns damit befassen, wie Fehler in früheren Versionen der Sprache untersucht und konstruiert wurden.

Fehler vor dem Start 1.13


Fehlerforschung


Fehler in Go sind Bedeutungen. Programme treffen Entscheidungen basierend auf diesen Werten auf unterschiedliche Weise. Meistens wird der Fehler mit nil verglichen, um festzustellen, ob der Vorgang fehlgeschlagen ist.

 if err != nil { // something went wrong } 

Manchmal vergleichen wir den Fehler, um den Kontrollwert herauszufinden und festzustellen, ob ein bestimmter Fehler aufgetreten ist.

 var ErrNotFound = errors.New("not found") if err == ErrNotFound { // something wasn't found } 

Der Fehlerwert kann von einem beliebigen Typ sein, der die in der Sprache definierte Fehlerschnittstelle erfüllt. Ein Programm kann eine Typanweisung oder einen Typschalter verwenden, um den Fehlerwert eines spezifischeren Typs anzuzeigen.

 type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + ": not found" } if e, ok := err.(*NotFoundError); ok { // e.Name wasn't found } 

Informationen hinzufügen


Oft leitet eine Funktion einen Fehler an den Aufrufstapel weiter und fügt ihm Informationen hinzu, z. B. eine kurze Beschreibung dessen, was passiert ist, als der Fehler aufgetreten ist. Dies ist einfach zu tun. Erstellen Sie einfach einen neuen Fehler, der den Text des vorherigen Fehlers enthält:

 if err != nil { return fmt.Errorf("decompress %v: %v", name, err) } 

Wenn Sie mit fmt.Errorf einen neuen Fehler fmt.Errorf verwerfen wir alles außer dem Text des ursprünglichen Fehlers. Wie wir im QueryError Beispiel gesehen haben, müssen Sie manchmal einen neuen Fehlertyp definieren, der den ursprünglichen Fehler enthält, um ihn zur Analyse mithilfe von Code zu speichern:

 type QueryError struct { Query string Err error } 

Programme können in den *QueryError und eine Entscheidung basierend auf dem ursprünglichen Fehler treffen. Dies wird manchmal als Auspacken eines Fehlers bezeichnet.

 if e, ok := err.(*QueryError); ok && e.Err == ErrPermission { // query failed because of a permission problem } 

Der Typ os.PathError aus der Standardbibliothek ist ein weiteres Beispiel dafür, wie ein Fehler einen anderen enthält.

Fehler in Go 1.13


Methode auspacken


In Go 1.13 vereinfachten die Standardbibliothekspakete errors und fmt die fmt Fehlern, die andere Fehler enthalten. Das wichtigste ist die Konvention, nicht die Änderung: Ein Fehler, der einen anderen Fehler enthält, kann die Unwrap Methode implementieren, die den ursprünglichen Fehler zurückgibt. Wenn e1.Unwrap() e2 zurückgibt, sagen wir, dass e1 e2 packt und Sie e1 entpacken können, um e2 zu erhalten.

Gemäß dieser Konvention können Sie der QueryError beschriebenen QueryError Methode den QueryError beschriebenen QueryError Typ QueryError , der den darin enthaltenen Fehler zurückgibt:

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

Das Ergebnis des Entpackens des Fehlers kann auch die Unwrap Methode enthalten. Die Folge von Fehlern, die durch wiederholtes Auspacken erhalten werden, nennen wir die Fehlerkette .

Fehleruntersuchung mit Is und As


In Go 1.13 enthält das errors zwei neue Funktionen zur Fehleruntersuchung: Is und As .

Die Funktion errors.Is vergleicht einen Fehler mit einem Wert.

 // Similar to: // if err == ErrNotFound { … } if errors.Is(err, ErrNotFound) { // something wasn't found } 

Die As Funktion prüft, ob der Fehler von einem bestimmten Typ ist.

 // Similar to: // if e, ok := err.(*QueryError); ok { … } var e *QueryError if errors.As(err, &e) { // err is a *QueryError, and e is set to the error's value } 

Im einfachsten Fall verhält sich die Funktion errors.Is wie ein Vergleich mit einem Steuerfehler und die Funktion errors.As wie eine errors.As . Bei der Arbeit mit gepackten Fehlern bewerten diese Funktionen jedoch alle Fehler in der Kette. Schauen wir uns das QueryError Beispiel oben an, um den ursprünglichen Fehler zu untersuchen:

 if e, ok := err.(*QueryError); ok && e.Err == ErrPermission { // query failed because of a permission problem } 

Mit der Funktion errors.Is können errors.Is schreiben:

 if errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem } 

Das Unwrap enthält auch eine neue Unwrap Funktion, die das Ergebnis des Aufrufs der Unwrap Methode des Fehlers zurückgibt oder nil zurückgibt, wenn der Fehler nicht über die Unwrap Methode verfügt. Es ist normalerweise besser, errors.Is zu verwenden. errors.Is oder errors.As , da Sie damit die gesamte Kette mit einem einzigen Aufruf untersuchen können.

Fehler beim Verpacken mit% w


Wie bereits erwähnt, ist es üblich, die Funktion fmt.Errorf zu verwenden, um dem Fehler zusätzliche Informationen hinzuzufügen.

 if err != nil { return fmt.Errorf("decompress %v: %v", name, err) } 

In Go 1.13 unterstützt die Funktion fmt.Errorf den neuen Befehl %w . Wenn dies der fmt.Errorf ist, enthält der von fmt.Errorf Fehler die Unwrap Methode, die das %w Argument zurückgibt, das ein Fehler sein sollte. In allen anderen Fällen ist %w identisch mit %v .

 if err != nil { // Return an error which unwraps to err. return fmt.Errorf("decompress %v: %w", name, err) } 

Durch das Packen des Fehlers mit %w wird er für errors.Is verfügbar. errors.Is und errors.As :

 err := fmt.Errorf("access denied: %w", ErrPermission) ... if errors.Is(err, ErrPermission) ... 

Wann packen?


Wenn Sie dem Fehler mit fmt.Errorf oder einer benutzerdefinierten fmt.Errorf einen zusätzlichen Kontext hinzufügen, müssen Sie entscheiden, ob der neue Fehler das Original enthält. Es gibt keine einzige Antwort darauf, alles hängt vom Kontext ab, in dem der neue Fehler erstellt wird. Packen Sie, um ihren Anrufer zu zeigen. Packen Sie den Fehler nicht ein, wenn dies zur Offenlegung von Implementierungsdetails führt.

Stellen Sie sich beispielsweise eine Parse , die eine komplexe Datenstruktur aus io.Reader liest. Wenn ein Fehler auftritt, möchten wir die Nummer der Zeile und Spalte herausfinden, in der er aufgetreten ist. Wenn beim Lesen von io.Reader ein Fehler aufgetreten ist, müssen wir ihn packen, um den Grund herauszufinden. Da dem Aufrufer die Funktion io.Reader zur io.Reader , ist es sinnvoll, den von ihm erzeugten Fehler io.Reader .

Ein anderer Fall: Eine Funktion, die mehrere Datenbankaufrufe ausführt, sollte wahrscheinlich keinen Fehler zurückgeben, bei dem das Ergebnis eines dieser Aufrufe gepackt ist. Wenn die von dieser Funktion verwendete Datenbank Teil der Implementierung ist, verstößt das Offenlegen dieser Fehler gegen die Abstraktion. Wenn die LookupUser Funktion aus dem Paket pkg Paket Go database/sql , tritt möglicherweise der Fehler sql.ErrNoRows . Wenn Sie mit fmt.Errorf("accessing DB: %v", err) einen Fehler fmt.Errorf("accessing DB: %v", err) , kann der Aufrufer nicht nach innen schauen und sql.ErrNoRows finden. Wenn die Funktion jedoch fmt.Errorf("accessing DB: %w", err) , kann der Aufrufer fmt.Errorf("accessing DB: %w", err) schreiben:

 err := pkg.LookupUser(...) if errors.Is(err, sql.ErrNoRows) … 

In diesem Fall sollte die Funktion immer sql.ErrNoRows wenn Sie Clients nicht sql.ErrNoRows möchten, auch wenn Sie zu einem Paket mit einer anderen Datenbank wechseln. Mit anderen Worten, das Packen macht einen Fehler zu einem Teil Ihrer API. Wenn Sie diesen Fehler in Zukunft nicht mehr als Teil der API unterstützen möchten, packen Sie ihn nicht.

Es ist wichtig zu beachten, dass der Fehler unabhängig davon, ob Sie ihn einpacken oder nicht, unverändert bleibt. Eine Person, die es verstehen wird, wird die gleichen Informationen haben. Entscheidungen über Verpackungen zu treffen, hängt davon ab, ob zusätzliche Informationen für Programme benötigt werden, damit sie fundiertere Entscheidungen treffen können. oder wenn Sie diese Informationen ausblenden möchten, um den Abstraktionsgrad beizubehalten.

Einrichten von Fehlertests mit Is- und As-Methoden


Die Funktion errors.Is jeden Fehler in der Kette mit dem Zielwert. Standardmäßig entspricht ein Fehler diesem Wert, wenn sie gleichwertig sind. Darüber hinaus kann ein Fehler in der Kette mithilfe der Implementierung der Is Methode die Übereinstimmung mit dem Zielwert erklären.

Betrachten Sie den Fehler, der durch das Upspin-Paket verursacht wird , das den Fehler mit der Vorlage vergleicht und nur Felder ungleich Null auswertet:

 type Error struct { Path string User string } func (e *Error) Is(target error) bool { t, ok := target.(*Error) if !ok { return false } return (e.Path == t.Path || t.Path == "") && (e.User == t.User || t.User == "") } if errors.Is(err, &Error{User: "someuser"}) { // err's User field is "someuser". } 

Die Funktion errors.As empfiehlt auch die As Methode, falls vorhanden.

Fehler und Paket-APIs


Ein Paket, das Fehler zurückgibt (und die meisten Pakete tun dies), sollte die Eigenschaften dieser Fehler beschreiben, auf die sich ein Programmierer verlassen kann. Ein gut gestaltetes Paket vermeidet auch die Rückgabe von Fehlern mit Eigenschaften, auf die man sich nicht verlassen kann.

Am einfachsten ist es zu sagen, ob die Operation erfolgreich war, und den Wert null bzw. nicht null zurückzugeben. In vielen Fällen sind keine weiteren Informationen erforderlich.

Wenn Sie die Funktion benötigen, um einen identifizierbaren Fehlerzustand zurückzugeben, z. B. "Element nicht gefunden", können Sie einen Fehler zurückgeben, in den der Signalwert gepackt ist.

 var ErrNotFound = errors.New("not found") // FetchItem returns the named item. // // If no item with the name exists, FetchItem returns an error // wrapping ErrNotFound. func FetchItem(name string) (*Item, error) { if itemNotFound(name) { return nil, fmt.Errorf("%q: %w", name, ErrNotFound) } // ... } 

Es gibt andere Muster zum Bereitstellen von Fehlern, die der Aufrufer semantisch untersuchen kann. Geben Sie beispielsweise direkt einen Steuerwert, einen bestimmten Typ oder einen Wert zurück, der mithilfe einer Prädikativfunktion analysiert werden kann.

Geben Sie die internen Details in keinem Fall an den Benutzer weiter. Wie im Kapitel „Wann lohnt es sich zu verpacken?“ Erwähnt: Wenn Sie einen Fehler von einem anderen Paket zurückgeben, konvertieren Sie ihn so, dass der ursprüngliche Fehler nicht angezeigt wird, es sei denn, Sie beabsichtigen, diesen bestimmten Fehler in Zukunft zurückzugeben.

 f, err := os.Open(filename) if err != nil { // The *os.PathError returned by os.Open is an internal detail. // To avoid exposing it to the caller, repackage it as a new // error with the same text. We use the %v formatting verb, since // %w would permit the caller to unwrap the original *os.PathError. return fmt.Errorf("%v", err) } 

Wenn eine Funktion einen Fehler mit einem gepackten Signalwert oder -typ zurückgibt, wird der ursprüngliche Fehler nicht direkt zurückgegeben.

 var ErrPermission = errors.New("permission denied") // DoSomething returns an error wrapping ErrPermission if the user // does not have permission to do something. func DoSomething() { if !userHasPermission() { // If we return ErrPermission directly, callers might come // to depend on the exact error value, writing code like this: // // if err := pkg.DoSomething(); err == pkg.ErrPermission { … } // // This will cause problems if we want to add additional // context to the error in the future. To avoid this, we // return an error wrapping the sentinel so that users must // always unwrap it: // // if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... } return fmt.Errorf("%w", ErrPermission) } // ... } 

Fazit


Obwohl wir nur drei Funktionen und einen Formatierungsbefehl besprochen haben, hoffen wir, dass sie dazu beitragen, die Fehlerbehandlung in Go-Programmen erheblich zu verbessern. Wir hoffen, dass das Verpacken, um zusätzlichen Kontext bereitzustellen, zur normalen Praxis wird und Programmierern hilft, bessere Entscheidungen zu treffen und Fehler schneller zu finden.

Wie Russ Cox in seiner Rede auf der GopherCon 2019 sagte, experimentieren, vereinfachen und versenden wir auf dem Weg zu Go 2. Und jetzt, nachdem wir diese Änderungen ausgeliefert haben, machen wir uns an neue Experimente.

Source: https://habr.com/ru/post/de473658/


All Articles