Nicht ohne Panik in Go

Hallo, liebe Leser von Habrahabr. Während ich ein mögliches neues Design für die Fehlerbehandlung diskutiere und über die Vorteile der expliziten Fehlerbehandlung diskutiere, schlage ich vor, einige der Merkmale von Fehlern, Panik und deren Wiederherstellung in Go zu berücksichtigen, die in der Praxis nützlich sein werden.
Bild


Fehler


Fehler ist eine Schnittstelle. Und wie bei den meisten Schnittstellen in Go ist die Definition von Fehlern kurz und einfach:


type error interface { Error() string } 

Es stellt sich heraus, dass jeder Typ mit der Error-Methode als Fehler verwendet werden kann. Wie Rob Pike lehrte, sind Fehler Werte , und Werte können verwendet werden, um verschiedene Logik zu manipulieren und zu programmieren.


In der Go-Standardbibliothek gibt es zwei Funktionen, mit denen Fehler bequem erstellt werden können. Die Funktion error.New eignet sich gut zum Erstellen einfacher Fehler. Die Funktion fmt.Errorf ermöglicht die Verwendung der Standardformatierung.


 err := errors.New("emit macho dwarf: elf header corrupted") const name, id = "bimmler", 17 err := fmt.Errorf("user %q (id %d) not found", name, id) 

In der Regel reicht der Fehlertyp aus, um Fehler zu behandeln. Manchmal kann es jedoch erforderlich sein, zusätzliche Informationen mit einem Fehler zu übertragen. In solchen Fällen können Sie Ihren eigenen Fehlertyp hinzufügen.
Ein gutes Beispiel ist der Typ von PathError aus dem Betriebssystempaket


 // PathError records an error and the operation and file path that caused it. type PathError struct { Op string Path string Err error } func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() } 

Der Wert eines solchen Fehlers enthält die Operation, den Pfad und den Fehler.


Sie werden folgendermaßen initialisiert:


 ... return nil, &PathError{"open", name, syscall.ENOENT} ... return nil, &PathError{"close", file.name, e} 

Die Verarbeitung kann eine Standardform haben:


 _, err := os.Open("---") if err != nil{ fmt.Println(err) } // open ---: The system cannot find the file specified. 

Wenn Sie jedoch zusätzliche Informationen benötigen, können Sie den Fehler in * os.PathError entpacken:


 _, err := os.Open("---") if pe, ok := err.(*os.PathError);ok{ fmt.Printf("Err: %s\n", pe.Err) fmt.Printf("Op: %s\n", pe.Op) fmt.Printf("Path: %s\n", pe.Path) } // Err: The system cannot find the file specified. // Op: open // Path: --- 

Der gleiche Ansatz kann angewendet werden, wenn die Funktion mehrere verschiedene Fehlertypen zurückgeben kann.
spielen


Erklärung verschiedener Arten von Fehlern, jeder hat seine eigenen Daten:


Code
 type ErrTimeout struct { Time time.Duration Err error } func (e *ErrTimeout) Error() string { return e.Time.String() + ": " + e.Err.Error() } type ErrPermission struct { Status string Err error } func (e *ErrPermission) Error() string { return e.Status + ": " + e.Err.Error() } 

Eine Funktion, die diese Fehler zurückgeben kann:


Code
 func proc(n int) error { if n <= 10 { return &ErrTimeout{Time: time.Second * 10, Err: errors.New("timeout error")} } else if n >= 10 { return &ErrPermission{Status: "access_denied", Err: errors.New("permission denied")} } return nil } 

Fehlerbehandlung durch Typumwandlungen:


Code
 func main(){ err := proc(11) if err != nil { switch e := err.(type) { case *ErrTimeout: fmt.Printf("Timeout: %s\n", e.Time.String()) fmt.Printf("Error: %s\n", e.Err) case *ErrPermission: fmt.Printf("Status: %s\n", e.Status) fmt.Printf("Error: %s\n", e.Err) default: fmt.Println("hm?") os.Exit(1) } } } 

Für den Fall, dass Fehler keine speziellen Eigenschaften benötigen, empfiehlt es sich, in Go Variablen zum Speichern von Fehlern auf Paketebene zu erstellen. Ein Beispiel sind Fehler wie io.EOF, io.ErrNoProgress usw.


Im folgenden Beispiel unterbrechen wir das Lesen und führen die Anwendung weiter aus, wenn der Fehler io.EOF ist, oder wir schließen die Anwendung für andere Fehler.


 func main(){ reader := strings.NewReader("hello world") p := make([]byte, 2) for { _, err := reader.Read(p) if err != nil{ if err == io.EOF { break } log.Fatal(err) } } } 

Dies ist effektiv, da Fehler nur einmal generiert und wiederverwendet werden.


Stapelverfolgung


Liste der Funktionen, die zum Zeitpunkt der Stapelerfassung aufgerufen wurden. Mithilfe der Stapelverfolgung erhalten Sie eine bessere Vorstellung davon, was im System vor sich geht. Das Speichern des Trace in den Protokollen kann beim Debuggen sehr hilfreich sein.


Go fehlen diese Informationen oft fälschlicherweise, aber zum Glück ist es nicht schwierig, einen Dump in Go zu bekommen.


Sie können debug.PrintStack () verwenden, um den Trace in die Standardausgabe auszugeben:


 func main(){ foo() } func foo(){ bar() } func bar(){ debug.PrintStack() } 

Infolgedessen werden die folgenden Informationen an Stderr geschrieben:


Stapel
 goroutine 1 [running]: runtime/debug.Stack(0x1, 0x7, 0xc04207ff78) .../Go/src/runtime/debug/stack.go:24 +0xae runtime/debug.PrintStack() .../Go/src/runtime/debug/stack.go:16 +0x29 main.bar() .../main.go:13 +0x27 main.foo() .../main.go:10 +0x27 main.main() .../main.go:6 +0x27 

debug.Stack () gibt ein Byte-Slice mit einem Stack-Dump zurück, der später oder an anderer Stelle protokolliert werden kann.


 b := debug.Stack() fmt.Printf("Trace:\n %s\n", b) 

Es gibt noch einen anderen Punkt, wenn uns das gefällt:


 go bar() 

dann erhalten wir folgende Informationen am Ausgang:


 main.bar() .../main.go:19 +0x2d created by main.foo .../main.go:14 +0x3c 

Jede Goroutine hat einen eigenen Stack, wir bekommen nur ihren Dump. Übrigens haben Goroutinen ihre eigenen Stapel, die Wiederherstellung ist immer noch damit verbunden, aber dazu später mehr.
Um Informationen zu allen Goroutinen anzuzeigen, können Sie runtime.Stack () verwenden und das zweite Argument true übergeben.


 func bar(){ buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } fmt.Printf("Trace:\n %s\n", buf) } 

Stapel
 Trace: goroutine 5 [running]: main.bar() .../main.go:21 +0xbc created by main.foo .../main.go:14 +0x3c goroutine 1 [sleep]: time.Sleep(0x77359400) .../Go/src/runtime/time.go:102 +0x17b main.foo() .../main.go:16 +0x49 main.main() .../main.go:10 +0x27 

Fügen Sie diese Informationen zum Fehler hinzu und erhöhen Sie dadurch den Informationsgehalt erheblich.
Zum Beispiel so:


 type ErrStack struct { StackTrace []byte Err error } func (e *ErrStack) Error() string { var buf bytes.Buffer fmt.Fprintf(&buf, "Error:\n %s\n", e.Err) fmt.Fprintf(&buf, "Trace:\n %s\n", e.StackTrace) return buf.String() } 

Sie können eine Funktion hinzufügen, um diesen Fehler zu erstellen:


 func NewErrStack(msg string) *ErrStack { buf := make([]byte, 1024) for { n := runtime.Stack(buf, true) if n < len(buf) { break } buf = make([]byte, 2*len(buf)) } return &ErrStack{StackTrace: buf, Err: errors.New(msg)} } 

Dann können Sie schon damit arbeiten:


 func main() { err := foo() if err != nil { fmt.Println(err) } } func foo() error{ return bar() } func bar() error{ err := NewErrStack("error") return err } 

Stapel
 Error: error Trace: goroutine 1 [running]: main.NewErrStack(0x4c021f, 0x5, 0x4a92e0) .../main.go:41 +0xae main.bar(0xc04207ff38, 0xc04207ff78) .../main.go:24 +0x3d main.foo(0x0, 0x48ebff) .../main.go:21 +0x29 main.main() .../main.go:11 +0x29 

Dementsprechend können der Fehler und die Ablaufverfolgung geteilt werden:


 func main(){ err := foo() if st, ok := err.(*ErrStack);ok{ fmt.Printf("Error:\n %s\n", st.Err) fmt.Printf("Trace:\n %s\n", st.StackTrace) } } 

Und natürlich gibt es bereits eine fertige Lösung. Eines davon ist das Paket https://github.com/pkg/errors . Sie können einen neuen Fehler erstellen, der bereits den Ablaufverfolgungsstapel enthält, und einem vorhandenen Fehler eine Ablaufverfolgung und / oder eine zusätzliche Nachricht hinzufügen. Plus bequeme Ausgabeformatierung.


 import ( "fmt" "github.com/pkg/errors" ) func main(){ err := foo() if err != nil { fmt.Printf("%+v", err) } } func foo() error{ err := bar() return errors.Wrap(err, "error2") } func bar() error{ return errors.New("error") } 

Stapel
 error main.bar .../main.go:20 main.foo .../main.go:16 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 error2 main.foo .../main.go:17 main.main .../main.go:9 runtime.main .../Go/src/runtime/proc.go:198 runtime.goexit .../Go/src/runtime/asm_amd64.s:2361 

% v zeigt nur Nachrichten an


 error2: error 

Panik / erholen


Panik (auch bekannt als Unfall, auch bekannt als Panik) signalisiert in der Regel das Vorhandensein von Fehlfunktionen, aufgrund derer das System (oder ein bestimmtes Subsystem) nicht weiter funktionieren kann. Wenn Panik ausgelöst wird, überprüft die Go-Laufzeit den Stapel und versucht, einen Handler dafür zu finden.


Unverarbeitete Panik beendet die Anwendung. Dies unterscheidet sie grundlegend von Fehlern, die es Ihnen ermöglichen, sich nicht selbst zu verarbeiten.


Sie können ein beliebiges Argument an den Panikfunktionsaufruf übergeben.


 panic(v interface{}) 

In Panik ist es praktisch, einen Fehler zu übergeben, der die Wiederherstellung vereinfacht und das Debuggen erleichtert.


 panic(errors.New("error")) 

Die Notfallwiederherstellung in Go basiert auf einem verzögerten Funktionsaufruf, auch als verzögert bezeichnet. Es wird garantiert, dass eine solche Funktion bei Rückkehr von der übergeordneten Funktion ausgeführt wird. Unabhängig vom Grund - die return-Anweisung, das Ende der Funktion oder Panik.


Und jetzt ermöglicht die Wiederherstellungsfunktion , Informationen über den Unfall abzurufen und das Abwickeln des Aufrufstapels zu stoppen.
Ein typischer Panikruf und Handler:


 func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() foo() } func foo(){ panic(errors.New("error")) } 

Wiederherstellen gibt die Schnittstelle {} (die wir an Panik übergeben) oder Null zurück, wenn kein Aufruf zur Panik aufgetreten ist.


Betrachten Sie ein weiteres Beispiel für die Notfallbehandlung. Wir haben eine Funktion, auf die wir beispielsweise eine Ressource übertragen und die theoretisch Panik auslösen kann.


 func bar(f *os.File) { panic(errors.New("error")) } 

Erstens müssen Sie möglicherweise am Ende immer einige Aktionen ausführen, z. B. das Bereinigen von Ressourcen, in unserem Fall das Schließen der Datei.


Zweitens sollte die fehlerhafte Ausführung einer solchen Funktion nicht zum Ende des gesamten Programms führen.


Dieses Problem kann durch Aufschieben, Wiederherstellen und Schließen gelöst werden:


 func foo()(err error) { file, _ := os.Open("file") defer func() { if r := recover(); r != nil { err = r.(error) //   ,   ,     // err := errors.New("trapped panic: %s (%T)", r, r) //     } file.Close() //   }() bar(file) return err } 

Durch das Schließen können wir uns den oben deklarierten Variablen zuwenden. Dank dieser Garantie können wir die Datei schließen und im Falle eines Unfalls einen Fehler daraus extrahieren und an den üblichen Fehlerbehandlungsmechanismus übergeben.


Es gibt umgekehrte Situationen, in denen eine Funktion mit bestimmten Argumenten immer korrekt funktionieren sollte. Wenn dies nicht der Fall ist, ist dies sehr schlecht.


Fügen Sie in solchen Fällen eine Wrapper-Funktion hinzu, in der die Zielfunktion aufgerufen wird, und im Fehlerfall wird Panik aufgerufen.


Go hat normalerweise Must- Präfixe:


 // MustCompile is like Compile but panics if the expression cannot be parsed. // It simplifies safe initialization of global variables holding compiled regular // expressions. func MustCompile(str string) *Regexp { regexp, error := Compile(str) if error != nil { panic(`regexp: Compile(` + quote(str) + `): ` + error.Error()) } return regexp } 

 // Must is a helper that wraps a call to a function returning (*Template, error) // and panics if the error is non-nil. It is intended for use in variable initializations // such as // var t = template.Must(template.New("name").Parse("html")) func Must(t *Template, err error) *Template { if err != nil { panic(err) } return t } 

Es lohnt sich, sich noch an eine Sache zu erinnern, die mit Panik und Goroutinen zu tun hat.


Ein Teil der Thesen aus dem, was oben diskutiert wurde:


  • Für jede Goroutine wird ein separater Stapel zugewiesen.
  • Wenn Sie Panik aufrufen, wird die Wiederherstellung auf dem Stapel gesucht.
  • Wenn keine Wiederherstellung gefunden wird, wird die gesamte Anwendung beendet.

Der Handler in main fängt die Panik von foo nicht ab und das Programm stürzt ab:


 func main(){ defer func() { if err := recover(); err != nil{ fmt.Printf("panic: %s", err) } }() go foo() time.Sleep(time.Minute) } func foo(){ panic(errors.New("error")) } 

Dies ist ein Problem, wenn beispielsweise ein Handler aufgerufen wird, um eine Verbindung zum Server herzustellen. Im Falle einer Panik in einem der Handler schließt der gesamte Server die Ausführung ab. Aus irgendeinem Grund können Sie die Behandlung von Unfällen in diesen Funktionen nicht steuern.
In einem einfachen Fall könnte die Lösung ungefähr so ​​aussehen:


 type f func() func Def(fn f) { go func() { defer func() { if err := recover(); err != nil { log.Println("panic") } }() fn() }() } func main() { Def(foo) time.Sleep(time.Minute) } func foo() { panic(errors.New("error")) } 

handhaben / prüfen


Vielleicht werden wir in Zukunft Änderungen in der Fehlerbehandlung sehen. Sie können sie unter den folgenden Links kennenlernen:
go2draft
Fehlerbehandlung in Go 2


Das ist alles für heute. Vielen Dank!

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


All Articles