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 {
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 {
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 {
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 {
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.
Die
As
Funktion prüft, ob der Fehler von einem bestimmten Typ ist.
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 {
Mit der Funktion
errors.Is
können
errors.Is
schreiben:
if errors.Is(err, ErrPermission) {
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 {
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"}) {
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")
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 {
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")
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.