Fehlerbehebung durch Fehlerbehebung



Go2 zielt darauf ab, den Aufwand für die Fehlerbehandlung zu reduzieren. Wussten Sie jedoch, was besser ist als die verbesserte Syntax für die Fehlerbehandlung?

Fehler müssen überhaupt nicht behandelt werden. Ich sage nicht "Löschen Sie Ihren Fehlerbehandlungscode", sondern schlage vor, Ihren Code so zu ändern, dass Sie nicht viele Fehler behandeln müssen.

Dieser Artikel wurde vom Kapitel „Definieren von Fehlern aus der Existenz heraus“ des Buches „ Eine Philosophie des Software-Designs “ von John Ousterhout inspiriert. Ich werde versuchen, seinen Rat auf Go anzuwenden.

Erstes Beispiel


Hier ist die Funktion zum Zählen der Anzahl der Zeilen in einer Datei:

func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err = br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, err } return lines, nil } 

Wir erstellen bufio.Reader, setzen uns dann in eine Schleife, rufen die ReadString-Methode auf, erhöhen den Zähler bis zum Ende der Datei und geben dann die Anzahl der gelesenen Zeilen zurück. Dies ist der Code, den wir schreiben wollten. Stattdessen wird CountLines durch die Fehlerbehandlung kompliziert.

Zum Beispiel gibt es so eine seltsame Konstruktion:

 _, err = br.ReadString('\n') lines++ if err != nil { break } 

Wir erhöhen die Anzahl der Zeilen, bevor wir nach Fehlern suchen - das sieht seltsam aus. Der Grund, warum wir es so schreiben sollten, ist, dass ReadString einen Fehler zurückgibt, wenn es auf das Ende der Datei - io.EOF - stößt, bevor das Zeilenumbruchzeichen gedrückt wird. Dies kann auch passieren, wenn kein Zeilenumbruch vorhanden ist.

Um dieses Problem zu lösen, werden wir die Logik neu organisieren, um die Anzahl der Zeilen zu erhöhen, und dann prüfen, ob wir die Schleife verlassen müssen (diese Logik ist immer noch nicht korrekt, können Sie einen Fehler finden?).

Wir haben jedoch noch nicht nach Fehlern gesucht. ReadString gibt io.EOF zurück, wenn das Ende der Datei erreicht ist. Dies wird erwartet, ReadString braucht eine Möglichkeit, um Stop zu sagen, es gibt nichts mehr zu lesen. Bevor wir den Fehler an den Aufrufer von CountLine zurücksenden, müssen wir daher überprüfen, ob der io.EOF-Fehler nicht gefunden wurde, und ihn in diesem Fall an den Aufrufer zurückgeben, andernfalls geben wir null zurück, wenn alles in Ordnung ist. Aus diesem Grund ist die letzte Zeile der Funktion nicht einfach

 return lines, err 

Ich denke, dies ist ein gutes Beispiel für die Beobachtung von Russ Cox, dass die Fehlerbehandlung die Funktion erschweren kann . Schauen wir uns die verbesserte Version an.

 func CountLines(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() } 

Diese verbesserte Version wechselt von bufio.Reader zu bufio.Scanner. Unter der Haube verwendet bufio.Scanner bufio.Reader und fügt eine Abstraktionsschicht hinzu, mit deren Hilfe die Fehlerbehandlung beseitigt werden kann, die die Arbeit unserer vorherigen Version von CountLines behindert hat (bufio.Scanner kann jede Vorlage scannen und sucht standardmäßig nach neuen Zeilen).

Die sc.Scan () -Methode gibt true zurück, wenn der Scanner eine Textzeile gefunden und keinen Fehler gefunden hat. Daher wird der Hauptteil unserer for-Schleife nur aufgerufen, wenn sich eine Textzeile im Scannerpuffer befindet. Dies bedeutet, dass unsere überarbeiteten CountLines den Fall korrekt behandeln, wenn kein nachfolgendes Zeilenumbruchzeichen vorhanden ist. Auch jetzt wird der Fall, wenn die Datei leer ist, korrekt behandelt.

Zweitens, da sc.Scan bei einem Fehler false zurückgibt, endet unsere for-Schleife, wenn das Ende der Datei erreicht ist oder ein Fehler auftritt. Der Typ bufio.Scanner merkt sich den ersten erkannten Fehler, und wir beheben diesen Fehler, nachdem wir die Schleife mit der Methode sc.Err () verlassen haben.

Schließlich kümmert sich buffo.Scanner um die Verarbeitung von io.EOF und konvertiert es in nil, wenn das Ende der Datei fehlerfrei erreicht wird.

Zweites Beispiel


Mein zweites Beispiel ist inspiriert von Rob Pikes ' Errors are values blog post.

Beim Arbeiten mit dem Öffnen, Schreiben und Schließen von Dateien ist die Fehlerbehandlung zwar nicht sehr beeindruckend, da Vorgänge in Hilfsprogrammen wie ioutil.ReadFile und ioutil.WriteFile abgeschlossen werden können. Bei der Arbeit mit Netzwerkprotokollen auf niedriger Ebene ist es jedoch häufig erforderlich, eine Antwort direkt unter Verwendung von E / A-Grundelementen zu erstellen, sodass sich die Fehlerbehandlung möglicherweise wiederholt. Betrachten Sie dieses Fragment eines HTTP-Servers, der eine HTTP / 1.1-Antwort erstellt:

 type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprint(w, "\r\n"); err != nil { return err } _, err = io.Copy(w, body) return err } 

Zuerst erstellen wir mit fmt.Fprintf eine Statusleiste und suchen nach einem Fehler. Anschließend zeichnen wir für jeden Header den Schlüssel und den Wert des Headers auf und prüfen jedes Mal, ob ein Fehler vorliegt. Schließlich beenden wir den Header-Abschnitt mit einem zusätzlichen \ r \ n, überprüfen den Fehler und kopieren den Antworttext auf den Client. Obwohl wir den Fehler von io.Copy nicht überprüfen müssen, müssen wir ihn schließlich aus einem Formular mit zwei Rückgabewerten konvertieren, das io.Copy auf den einzelnen Rückgabewert zurückgibt, den WriteResponse erwartet.

Dies ist nicht nur eine Menge sich wiederholender Arbeit, jede Operation, die im Wesentlichen Bytes in io.Writer schreibt, hat eine andere Form der Fehlerbehandlung. Aber wir können unsere Aufgabe erleichtern, indem wir einen kleinen Wrapper einführen.

 type errWriter struct { io.Writer err error } func (e *errWriter) Write(buf []byte) (int, error) { if e.err != nil { return 0, e.err } var n int n, e.err = e.Writer.Write(buf) return n, nil } 

errWriter erfüllt den io.Writer-Vertrag, sodass ein vorhandener io.Writer migriert werden kann. errWriter überträgt die Aufzeichnungen an den zugrunde liegenden Rekorder, bis ein Fehler erkannt wird. Von nun an werden alle Einträge verworfen und der vorherige Fehler zurückgegeben.

 func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWriter{Writer: w} fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) } fmt.Fprint(ew, "\r\n") io.Copy(ew, body) return ew.err } 

Durch Anwenden von errWriter auf WriteResponse wird die Klarheit des Codes erheblich verbessert. Jede der Operationen muss sich nicht mehr auf die Fehlerprüfung beschränken. Die Fehlermeldung wird an das Ende der Funktion verschoben, überprüft das Feld ew.err und vermeidet die störende Übersetzung der zurückgegebenen io.Copy-Werte

Fazit


Wenn Sie auf eine übermäßige Fehlerbehandlung stoßen, versuchen Sie, einige Vorgänge als zusätzlichen Wrapper-Typ zu extrahieren.

Über den Autor


Der Autor dieses Artikels, Dave Cheney , ist Autor vieler beliebter Pakete für Go, zum Beispiel github.com/pkg/errors und github.com/davecheney/httpstat .

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


All Articles