Libérer la gestion des erreurs en éliminant les erreurs



Go2 vise à réduire les frais généraux de gestion des erreurs, mais saviez-vous quoi de mieux que la syntaxe améliorée pour la gestion des erreurs?

Pas besoin de gérer les erreurs. Je ne dis pas «supprimez votre code de gestion des erreurs», je suggère plutôt de changer votre code afin que vous n'ayez pas beaucoup d'erreurs à gérer.

Cet article s'inspire du chapitre «Définir les erreurs hors de l'existence» »du livre« Une philosophie de la conception de logiciels »de John Ousterhout. J'essaierai d'appliquer ses conseils à Go.

Premier exemple


Voici la fonction pour compter le nombre de lignes dans un fichier:

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 } 

Nous créons bufio.Reader, puis nous nous asseyons en boucle, appelant la méthode ReadString, augmentant le compteur jusqu'à la fin du fichier, puis renvoyant le nombre de lignes lues. C'est le code que nous voulions écrire, à la place CountLines est compliqué par la gestion des erreurs.

Par exemple, il y a une construction si étrange:

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

Nous augmentons le nombre de lignes avant de rechercher des erreurs - cela semble étrange. La raison pour laquelle nous devons l'écrire de cette façon est que ReadString retournera une erreur s'il rencontre la fin du fichier - io.EOF - avant d'appuyer sur le caractère de nouvelle ligne. Cela peut également se produire s'il n'y a pas de nouvelle ligne.

Pour résoudre ce problème, nous allons réorganiser la logique pour augmenter le nombre de lignes, puis voir si nous devons quitter la boucle (cette logique n'est toujours pas correcte, pouvez-vous trouver une erreur?).

Mais nous n'avons pas fini de vérifier les erreurs. ReadString renverra io.EOF lorsqu'il atteindra la fin du fichier. Cela est attendu, ReadString a besoin d'un moyen de dire stop, il n'y a plus rien à lire. Par conséquent, avant de renvoyer l'erreur à l'appelant de CountLine, nous devons vérifier si l'erreur io.EOF n'a pas été trouvée, et dans ce cas, la renvoyer à l'appelant, sinon nous retournerons nil lorsque tout ira bien. C'est pourquoi la dernière ligne de la fonction n'est pas facile

 return lines, err 

Je pense que c'est un bon exemple de l'observation de Russ Cox selon laquelle la gestion des erreurs peut rendre la fonction plus difficile . Regardons la version améliorée.

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

Cette version améliorée passe de l'utilisation de bufio.Reader à bufio.Scanner. Sous le capot, bufio.Scanner utilise bufio.Reader, ajoutant une couche d'abstraction qui aide à supprimer la gestion des erreurs, ce qui a gêné le travail de notre version précédente de CountLines (bufio.Scanner peut analyser n'importe quel modèle, par défaut, il recherche de nouvelles lignes).

La méthode sc.Scan () renvoie true si le scanner a trouvé une ligne de texte et n'a pas trouvé d'erreur. Ainsi, le corps de notre boucle for ne sera appelé que s'il y a une ligne de texte dans le tampon du scanner. Cela signifie que nos CountLines refaites traitent correctement le cas quand il n'y a pas de caractère de fin de ligne. De plus, le cas où le fichier est vide est correctement géré.

Deuxièmement, puisque sc.Scan renvoie false lorsqu'une erreur se produit, notre boucle for se termine lorsque la fin du fichier est atteinte ou qu'une erreur se produit. Tapez bufio.Scanner se souvient de la première erreur détectée, et nous corrigeons cette erreur après avoir quitté la boucle en utilisant la méthode sc.Err ().

Enfin, buffo.Scanner s'occupe du traitement de io.EOF et le convertit en nil si la fin du fichier est atteinte sans erreur.

Deuxième exemple


Mon deuxième exemple est inspiré de l'article de blog Les erreurs sont des valeurs de Rob Pikes.

Lorsque vous travaillez avec l'ouverture, l'écriture et la fermeture de fichiers, la gestion des erreurs est, mais pas très impressionnante, car les opérations peuvent être conclues dans des assistants tels que ioutil.ReadFile et ioutil.WriteFile. Cependant, lorsque vous travaillez avec des protocoles réseau de bas niveau, il est souvent nécessaire de construire une réponse directement à l'aide de primitives d'E / S, de sorte que la gestion des erreurs peut commencer à se répéter. Considérez ce fragment d'un serveur HTTP qui crée une réponse HTTP / 1.1:

 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 } 

Nous créons d'abord une barre d'état à l'aide de fmt.Fprintf et recherchons une erreur. Ensuite, pour chaque en-tête, nous enregistrons la clé et la valeur de l'en-tête, vérifiant à chaque fois une erreur. Enfin, nous terminons la section d'en-tête par un \ r \ n supplémentaire, vérifions l'erreur et copions le corps de la réponse sur le client. Enfin, bien que nous n'ayons pas besoin de vérifier l'erreur de io.Copy, nous devons la convertir à partir d'un formulaire avec deux valeurs de retour, que io.Copy renvoie à la seule valeur de retour attendue par WriteResponse.

Ce n'est pas seulement beaucoup de travail répétitif, chaque opération, qui consiste essentiellement à écrire des octets dans io.Writer, a une forme différente de gestion des erreurs. Mais nous pouvons faciliter notre tâche en introduisant un petit emballage.

 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 remplit le contrat io.Writer, il peut donc être utilisé pour migrer un io.Writer existant. errWriter transfère les enregistrements vers son enregistreur sous-jacent jusqu'à ce qu'une erreur soit détectée. Désormais, il supprime toutes les entrées et renvoie l'erreur précédente.

 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 } 

L'application de errWriter à WriteResponse améliore considérablement la clarté du code. Chacune des opérations n'a plus besoin de se limiter à la vérification des erreurs. Le message d'erreur se déplace à la fin de la fonction, vérifiant le champ ew.err et évitant la traduction ennuyeuse des valeurs io.Copy retournées

Conclusion


Lorsque vous rencontrez une gestion d'erreur excessive, essayez d'extraire certaines opérations en tant que type d'encapsuleur auxiliaire.

À propos de l'auteur


L'auteur de cet article, Dave Cheney , est l'auteur de nombreux packages populaires pour Go, par exemple github.com/pkg/errors et github.com/davecheney/httpstat .

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


All Articles