Gestion des erreurs dans Go

Bonjour, citoyens Habrovsk! Le cours de développeur de Golang commence déjà à OTUS aujourd'hui , et nous considérons que c'est une excellente occasion de partager un autre article utile sur le sujet. Aujourd'hui, parlons de l'approche de Go face aux erreurs. Commençons!



Maîtriser la gestion pragmatique des erreurs dans votre code Go




Cet article fait partie de la série Avant de commencer, où nous explorons le monde de Golang, partageons des conseils et des idées que vous devez savoir lors de l'écriture de code dans Go afin que vous n'ayez pas à remplir vos propres bosses.

Je suppose que vous avez déjà au moins une expérience de base avec Go, mais si vous sentez qu'à un moment donné vous êtes tombé sur un matériau inconnu en discussion, n'hésitez pas à faire une pause, à explorer le sujet et à revenir.

Maintenant que nous avons frayé notre chemin, allons-y!

L'approche de Go à la gestion des erreurs est l'une des fonctionnalités les plus controversées et les plus mal utilisées. Dans cet article, vous apprendrez l'approche de Go face aux erreurs et comprendrez comment elles fonctionnent «sous le capot». Vous apprendrez quelques approches différentes, examinerez le code source de Go et la bibliothèque standard pour savoir comment les erreurs sont gérées et comment les utiliser. Vous apprendrez pourquoi les assertions de type jouent un rôle important dans leur traitement et vous verrez les modifications à venir au traitement des erreurs que vous prévoyez d'introduire dans Go 2.



Entrée


Tout d'abord: les erreurs dans Go ne font pas exception. Dave Cheney a écrit un blog épique à ce sujet, donc je vous y réfère et résume: dans d'autres langues, vous ne pouvez pas être sûr qu'une fonction peut lever ou non une exception. Au lieu de lever des exceptions, les fonctions Go prennent en charge plusieurs valeurs de retour , et par convention, cette fonction est généralement utilisée pour renvoyer le résultat d'une fonction avec une variable d'erreur.



Si, pour une raison quelconque, votre fonction échoue, vous devriez probablement en renvoyer le type d' error précédemment déclaré. Par convention, le retour d'une erreur signale à l'appelant le problème et le retour de zéro n'est pas considéré comme une erreur. Ainsi, vous ferez comprendre à l'appelant qu'un problème est survenu et qu'il doit y faire face: quiconque appelle votre fonction, il sait qu'il ne doit pas se fier au résultat avant de rechercher une erreur. Si l'erreur n'est pas nulle, il est obligé de la vérifier et de la traiter (consigner, retourner, maintenir, appeler une sorte de mécanisme de nouvelle tentative / nettoyage, etc.).


(3 // gestion des erreurs
5 // suite)

Ces extraits sont très courants dans Go, et certains les considèrent comme du code passe-partout. Le compilateur traite les variables inutilisées comme des erreurs de compilation, donc si vous n'allez pas vérifier les erreurs, vous devez les affecter à un identifiant vide . Mais aussi pratique que cela puisse être, les erreurs ne doivent pas être ignorées.


(4 // ignorer les erreurs n'est pas sûr, et vous ne devez pas vous fier au résultat avant de vérifier les erreurs)
le résultat ne peut pas être approuvé tant qu'il n'a pas vérifié les erreurs

Le retour d'erreur avec les résultats, ainsi que le système de type Go strict, compliquent grandement l'écriture de code balisé. Vous devez toujours supposer que la valeur d'une fonction est corrompue, sauf si vous avez vérifié l'erreur qu'elle a renvoyée, et en affectant l'erreur à un identificateur vide, vous ignorez explicitement que la valeur de votre fonction peut être corrompue.


L'identifiant vide est sombre et plein d'horreurs.

Go a panic mécanismes de panic et de recover , qui sont également décrits dans un autre article détaillé du blog Go . Mais ils ne sont pas destinés à simuler des exceptions. Selon Dave, "Quand vous paniquez dans Go, vous paniquez vraiment: ce n'est pas le problème de quelqu'un d'autre, c'est déjà un joueur." Ils sont mortels et entraînent un crash dans votre programme. Rob Pike a inventé le dicton «Ne paniquez pas», qui parle de lui-même: vous devriez probablement éviter ces mécanismes et renvoyer des erreurs à la place.

"Les erreurs sont les significations."
"Ne vous contentez pas de vérifier les erreurs, mais gérez-les avec élégance."
"Ne paniquez pas"
toutes les paroles de Rob Pike

Sous le capot


Interface d'erreur

Sous le capot, le type d'erreur est une interface simple avec une seule méthode , et si vous ne la connaissez pas, je vous recommande fortement de consulter ce post sur le blog officiel de Go.


interface d'erreur de la source

Faire ses propres erreurs n'est pas difficile. Il existe différentes approches des structures utilisateur qui implémentent la méthode de string Error() . Toute structure qui implémente cette méthode unique est considérée comme une valeur d'erreur valide et peut être renvoyée comme telle.

Examinons quelques-unes de ces approches.

Structure errorString intégrée


L'implémentation la plus courante et la plus répandue de l'interface d'erreur est la structure errorString . C'est l'implémentation la plus simple à laquelle vous pouvez penser.


Source: code source Go

Vous pouvez voir sa mise en œuvre simplifiée ici . Il ne contient qu'une string et cette chaîne est renvoyée par la méthode Error . Cette erreur de chaîne peut être formatée par nous sur la base de certaines données, par exemple, en utilisant fmt.Sprintf . Mais à part cela, il ne contient aucune autre fonctionnalité. Si vous avez appliqué des erreurs.Nouveau ou fmt.Errorf , alors vous l'avez déjà utilisé .


(13 // sortie :)

essayer

github.com/pkg/errors


Un autre exemple simple est le paquetage pkg / errors . À ne pas confondre avec le package d' errors intégré que vous avez découvert plus tôt, ce package fournit des fonctionnalités importantes supplémentaires, telles que l'encapsulation d'erreurs, l'expansion, le formatage et l'enregistrement de trace de pile. Vous pouvez installer le package en exécutant go get github.com/pkg/errors .



Dans les cas où vous devez attacher la trace de pile ou les informations de débogage nécessaires à vos erreurs, l'utilisation des fonctions New ou Errorf ce package fournit des erreurs qui sont déjà écrites dans votre trace de pile, et vous pouvez également attacher des métadonnées simples en l'utilisant capacités de formatage. Errorf implémente l'interface fmt.Formatter , c'est-à-dire que vous pouvez la formater en utilisant les runes du package fmt ( %s , %v , %+v , etc.).


(// 6 ou alternative)

Ce package présente également les fonctions errors.Wrap et errors.Wrapf . Ces fonctions ajoutent du contexte à l'erreur à l'aide d'un message et d'une trace de pile à l'endroit où elles ont été appelées. Ainsi, au lieu de simplement renvoyer l'erreur, vous pouvez l'envelopper avec du contexte et des données de débogage importantes.



Les wrappers d'erreur par d'autres erreurs prennent en charge la méthode d' Cause() error , qui renvoie leur erreur interne. En outre, ils peuvent être utilisés avec des errors.Cause(err error) error fonction d' errors.Cause(err error) error , qui extrait la principale erreur interne dans l'erreur d'enrubannage.

Gestion des erreurs


Homologation de type


Les assertions de type jouent un rôle important dans le traitement des erreurs. Vous les utiliserez pour extraire des informations de la valeur d'interface, et comme la gestion des erreurs est associée aux implémentations utilisateur de l'interface d' error , l'implémentation des instructions d' error est un outil très pratique.

Sa syntaxe est la même pour toutes ses applications - x.(T) si x a un type d'interface. x.(T) indique que x pas nil et que la valeur stockée dans x est de type T Dans les sections suivantes, nous examinerons deux façons d'utiliser les instructions de type - avec un type spécifique T et avec une interface de type T


(2 // syntaxe abrégée en sautant la variable booléenne ok
3 // panique: conversion d'interface: l'interface {} est nulle, pas une chaîne
6 // syntaxe étendue avec booléen ok
8 // ne panique pas, définit à la place ok false lorsque l'instruction est fausse
9 // maintenant nous pouvons utiliser s comme chaîne en toute sécurité)

sandbox: panique avec une syntaxe raccourcie, syntaxe étendue sûre

Remarque supplémentaire sur la syntaxe: une assertion de type peut être utilisée avec une syntaxe raccourcie (qui panique lorsqu'une instruction échoue) ou une syntaxe étendue (qui utilise la valeur logique OK pour indiquer le succès ou l'échec). Je recommande toujours de prendre allongé au lieu de raccourci, car je préfère vérifier la variable OK et ne pas gérer la panique.


Homologation de type T


Une instruction de type x.(T) avec une interface de type T confirme que x implémente l'interface de T Ainsi, vous pouvez garantir que la valeur d'interface implémente l'interface, et seulement si c'est le cas, vous pouvez utiliser ses méthodes.


(5 ... // affirme que x implémente l'interface du résolveur
6 ... // ici, nous pouvons déjà utiliser cette méthode en toute sécurité)

Pour comprendre comment cela peut être utilisé, examinons à nouveau pkg/errors . Vous connaissez déjà ce paquet d'erreurs, alors errors.Cause(err error) error la errors.Cause(err error) error .

Cette fonction reçoit une erreur et extrait l'erreur la plus profonde dont elle souffre (celle qui ne sert plus de wrapper pour une autre erreur). Cela peut sembler primitif, mais il y a beaucoup de grandes choses que vous pouvez apprendre de cette implémentation:


source: pkg / erreurs

La fonction reçoit la valeur d'erreur et ne peut pas supposer que l'argument err qu'elle reçoit est une erreur d'encapsuleur (prise en charge par la méthode Cause ). Par conséquent, avant d'appeler la méthode Cause , vous devez vous assurer que vous avez affaire à une erreur qui implémente cette méthode. En effectuant une instruction de type à chaque itération de la boucle for, vous pouvez vous assurer que la variable cause prend en charge la méthode Cause et pouvez en extraire des erreurs internes jusqu'à ce que vous trouviez une erreur qui n'a pas Cause .

En créant une interface locale simple contenant uniquement les méthodes dont vous avez besoin et en y appliquant une assertion, votre code est séparé des autres dépendances. L'argument que vous avez reçu ne doit pas nécessairement être une structure connue, il doit simplement s'agir d'une erreur. Tout type qui implémente les méthodes Error et Cause fera l'affaire. Ainsi, si vous implémentez la méthode Cause dans votre type d'erreur, vous pouvez utiliser cette fonction avec elle sans ralentissements.

Cependant, il y a un petit défaut à garder à l'esprit: les interfaces sont sujettes à changement, vous devez donc maintenir soigneusement le code afin que vos déclarations ne soient pas violées. N'oubliez pas de définir vos interfaces là où vous les utilisez, de les garder fines et soignées, et tout ira bien.

Enfin, si vous n'avez besoin que d'une seule méthode, il est parfois plus pratique de faire une déclaration sur une interface anonyme contenant uniquement la méthode sur laquelle vous comptez, c'est-à-dire v, ok := x.(interface{ F() (int, error) }) . L'utilisation d'interfaces anonymes peut vous aider à séparer votre code des dépendances possibles et à le protéger contre d'éventuelles modifications des interfaces.

Approbation de type T et de commutateur de type



Je préfère cette section en introduisant deux modèles de gestion des erreurs similaires qui souffrent de plusieurs défauts et pièges. Cela ne signifie pas qu'ils ne sont pas courants. Les deux peuvent être des outils pratiques dans les petits projets, mais ils ne s'adaptent pas bien.

La première est la deuxième version de l'assertion de type: une assertion de type x.(T) avec un type T spécifique est effectuée. Il prétend que la valeur de x est de type T , ou peut être convertie en type T


(2 // nous pouvons utiliser v comme mypkg.SomeErrorType)

Un autre est le modèle de commutateur de type. Type Switch combine une instruction switch avec une instruction type à l'aide du mot-clé type réservé. Ils sont particulièrement courants dans la gestion des erreurs, où la connaissance du type de base d'une erreur variable peut être très utile.


(3 // traitement ...
5 // traitement ...)

Le gros inconvénient des deux approches est qu'elles conduisent toutes deux à une liaison de code avec leurs dépendances. Les deux exemples doivent être familiers avec la structure SomeErrorType (qui doit évidemment être exportée) et doivent importer le package mypkg .
Dans les deux approches, lors de la gestion de vos erreurs, vous devez connaître le type et importer son package. La situation est aggravée lorsque vous traitez des erreurs dans les wrappers, où la cause de l'erreur peut être une erreur résultant d'une dépendance interne que vous ne connaissez pas et que vous ne devriez pas connaître.


(7 // traitement ...
9 // traitement ...)

Type Switch fait la distinction entre *MyStruct et MyStruct . Par conséquent, si vous n'êtes pas sûr d'avoir affaire à un pointeur ou à une instance réelle d'une structure, vous devrez fournir les deux options. De plus, comme dans le cas des commutateurs normaux, les cas dans Type Switch n'échouent pas, mais contrairement au Commutateur de type habituel, l'utilisation de la fallthrough interdite dans Type Switch, vous devez donc utiliser une virgule et fournir les deux options, ce qui est facile à oublier.



Pour résumer


C'est tout! Vous connaissez maintenant les erreurs et devez être prêt à corriger toutes les erreurs que votre application Go peut renvoyer (ou réellement retourner) sur votre chemin!
Les deux packages d' errors fournissent des approches simples mais importantes aux erreurs dans Go, et s'ils répondent à vos besoins, ils sont un excellent choix. Vous pouvez facilement implémenter vos propres structures d'erreur et profiter de la gestion des erreurs Go en les combinant avec pkg/errors .

Lorsque vous mettez à l'échelle des erreurs simples, l'utilisation correcte des instructions de type peut être un excellent outil pour gérer diverses erreurs. Soit en utilisant Type Switch, soit en validant le comportement de l'erreur et en vérifiant les interfaces qu'il implémente.

Et ensuite?


La gestion des erreurs dans Go est désormais très pertinente. Maintenant que vous avez les bases, vous vous demandez peut-être ce qui nous attend pour gérer les erreurs Go!

La prochaine version de Go 2 y prête beaucoup d'attention, et vous pouvez déjà jeter un œil à la version provisoire . De plus, au cours de dotGo 2019, Marcel van Lojuizen a eu une excellente conversation sur un sujet que je ne peux que recommander - «GO 2 error values ​​today» .

De toute évidence, il existe de nombreuses autres approches, trucs et astuces, et je ne peux pas les inclure tous dans un seul article! Malgré cela, j'espère que cela vous a plu et je vous verrai dans le prochain épisode de Avant de commencer !

Et maintenant, traditionnellement, attend vos commentaires.

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


All Articles