Gestion des erreurs dans Golang
À la différence des méthodes conventionnelles dans d’autres langages de programmation grand public tels que JavaScript (qui utilise l’instruction try… catch
) ou Python (avec son bloc try… except
) la gestion des erreurs dans Go nécessite une approche différente. Pourquoi ? Parce que ses fonctionnalités de gestion des erreurs sont souvent mal appliquées.
Dans ce billet de blog, nous allons jeter un coup d’œil aux meilleures pratiques qui pourraient être utilisées pour gérer les erreurs dans une application Go. Une compréhension de base du fonctionnement de Go est tout ce qui est nécessaire pour digérer cet article – si vous vous sentez bloqué à un moment donné, il n’y a pas de mal à prendre un peu de temps et à rechercher des concepts peu familiers.
L’identifiant vide
L’identifiant vide est un placeholder anonyme. Il peut être utilisé comme tout autre identifiant dans une déclaration, mais il n’introduit pas de liaison. L’identificateur vide fournit un moyen d’ignorer les valeurs gauches dans une affectation et d’éviter les erreurs du compilateur concernant les importations et les variables inutilisées dans un programme. La pratique consistant à affecter les erreurs à l’identificateur vide au lieu de les traiter correctement n’est pas sûre car cela signifie que vous avez décidé d’ignorer explicitement la valeur de la fonction définie.
result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}
Votre raison pour probablement faire cela est que vous ne vous attendez pas à une erreur de la fonction (ou toute autre erreur qui pourrait se produire) mais cela pourrait créer des effets en cascade dans votre programme. La meilleure chose à faire est de gérer une erreur chaque fois que vous le pouvez.
Gestion des erreurs par des valeurs de retour multiples
Une façon de gérer les erreurs est de profiter du fait que les fonctions en Go supportent des valeurs de retour multiples. Ainsi, vous pouvez passer une variable d’erreur à côté du résultat de la fonction que vous définissez :
func iterate(x, y int) (int, error) {}
Dans l’exemple de code ci-dessus, nous devons retourner la variable prédéfinie error
si nous pensons qu’il y a une chance que notre fonction échoue. error
est un type d’interface déclaré dans le paquet built-in
de Go et sa valeur zéro est nil
.
type error interface { Error() string }
En général, renvoyer une erreur signifie qu’il y a un problème et renvoyer nil
signifie qu’il n’y a pas eu d’erreur :
result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }
Donc, chaque fois que la fonction iterate
est appelée et que err
n’est pas égale à nil
, l’erreur renvoyée doit être traitée de manière appropriée – une option pourrait être de créer une instance d’un mécanisme de réessai ou de nettoyage. Le seul inconvénient à gérer les erreurs de cette façon est qu’il n’y a pas d’application de la part du compilateur de Go, vous devez décider de la façon dont la fonction que vous avez créée renvoie l’erreur. Vous pouvez définir une structure d’erreur et la placer dans la position des valeurs retournées. Une façon de le faire est d’utiliser la structure intégrée errorString
(vous pouvez également trouver ce code dans le code source de Go):
package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }
Dans l’exemple de code ci-dessus, errorString
incorpore une string
qui est retournée par la méthode Error
. Pour créer une erreur personnalisée, vous devrez définir votre structure d’erreur et utiliser des ensembles de méthodes pour associer une fonction à votre structure:
// Define an error structtype CustomError struct { msg string}// Create a function Error() string and associate it to the struct.func(error * CustomError) Error() string { return error.msg}// Then create an error object using MyError struct.func CustomErrorInstance() error { return &CustomError { "File type not supported" }}
L’erreur personnalisée nouvellement créée peut ensuite être restructurée pour utiliser la structure intégrée error
:
import "errors"func CustomeErrorInstance() error { return errors.New("File type not supported")}
Une limitation de la structure intégrée error
est qu’elle ne vient pas avec des traces de pile. Cela rend la localisation de l’endroit où une erreur s’est produite très difficile. L’erreur pourrait passer par un certain nombre de fonctions avant d’être imprimée. Pour gérer cela, vous pouvez installer le paquet pkg/errors
qui fournit des primitives de base pour la gestion des erreurs telles que l’enregistrement de la trace de la pile, l’enveloppement, le déballage et le formatage des erreurs. Pour installer ce paquet, exécutez cette commande dans votre terminal:
go get github.com/pkg/errors
Lorsque vous devez ajouter des traces de pile ou toute autre information qui facilite le débogage à vos erreurs, utilisez les fonctions New
ou Errorf
pour fournir des erreurs qui enregistrent votre trace de pile. Errorf
implémente l’interface fmt.Formatter
qui vous permet de formater vos erreurs en utilisant les runes du paquet fmt
(%s
, %v
, %+v
etc):
import( "github.com/pkg/errors" "fmt")func X() error { return errors.Errorf("Could not write to file")}func customError() { return X()}func main() { fmt.Printf("Error: %+v", customError())}
Pour imprimer des traces de pile au lieu d’un simple message d’erreur, vous devez utiliser %+v
au lieu de %v
dans le motif de format, et les traces de pile ressembleront à l’échantillon de code ci-dessous :
Error: Could not write to filemain.X /Users/raphaelugwu/Go/src/golangProject/error_handling.go:7main.customError /Users/raphaelugwu/Go/src/golangProject/error_handling.go:15main.main /Users/raphaelugwu/Go/src/golangProject/error_handling.go:19runtime.main /usr/local/opt/go/libexec/src/runtime/proc.go:192runtime.goexit /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471
Defer, panic, and recover
Bien que Go n’ait pas d’exceptions, il a un type de mécanisme similaire connu sous le nom de « Defer, panic, and recover ». L’idéologie de Go est que l’ajout d’exceptions telles que l’instruction try/catch/finally
en JavaScript entraînerait un code complexe et encouragerait les programmeurs à étiqueter comme exceptionnelles trop d’erreurs de base, telles que l’échec de l’ouverture d’un fichier. Vous ne devriez pas utiliser defer/panic/recover
comme vous le feriez avec throw/catch/finally
; seulement en cas d’échec inattendu et irrécupérable.
Defer
est un mécanisme de langage qui place votre appel de fonction dans une pile. Chaque fonction différée est exécutée dans l’ordre inverse lorsque la fonction hôte se termine, qu’une panique soit appelée ou non. Le mécanisme de report est très utile pour nettoyer les ressources:
package mainimport ( "fmt")func A() { defer fmt.Println("Keep calm!") B()}func B() { defer fmt.Println("Else...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") D()}func D() { defer fmt.Println("If it's more than 30 degrees...")}func main() { A()}
Ceci se compilerait comme:
If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!
Panic
est une fonction intégrée qui arrête le flux d’exécution normal. Lorsque vous appelez panic
dans votre code, cela signifie que vous avez décidé que votre appelant ne peut pas résoudre le problème. Ainsi, panic
ne devrait être utilisé que dans de rares cas où il n’est pas sûr pour votre code ou toute personne intégrant votre code de continuer à ce point. Voici un exemple de code illustrant le fonctionnement de panic
:
package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}
L’exemple ci-dessus se compilerait comme suit:
If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Then we can't save the earth!panic: Global Warming!!!goroutine 1 :main.Break() /tmp/sandbox186240156/prog.go:22 +0xe0main.C() /tmp/sandbox186240156/prog.go:18 +0xa0main.B() /tmp/sandbox186240156/prog.go:14 +0xa0main.A() /tmp/sandbox186240156/prog.go:10 +0xa0main.main() /tmp/sandbox186240156/prog.go:26 +0x20Program exited: status 2.
Comme indiqué ci-dessus, lorsque panic
est utilisé et non géré, le flux d’exécution s’arrête, toutes les fonctions différées sont exécutées dans l’ordre inverse et les traces de pile sont imprimées.
Vous pouvez utiliser la fonction intégrée recover
pour gérer panic
et renvoyer les valeurs passant d’un appel de panique. recover
doit toujours être appelée dans une fonction defer
sinon elle retournera nil
:
package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") defer func() { if x := recover(); x != nil { fmt.Printf("Panic: %+v\n", x) } }() B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}
Comme on peut le voir dans l’exemple de code ci-dessus, recover
empêche l’ensemble du flux d’exécution de s’arrêter parce que nous avons jeté dans une fonction panic
et que le compilateur retournerait :
If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Panic: Global Warming!!!Then we can't save the earth!Program exited.
Pour signaler une erreur comme valeur de retour, vous devez appeler la fonction recover
dans la même goroutine que la fonction panic
est appelée, récupérer une structure d’erreur de la fonction recover
, et la passer à une variable:
package mainimport ( "errors" "fmt")func saveEarth() (err error) { defer func() { if r := recover(); r != nil { err = r.(error) } }() TooLate() return}func TooLate() { A() panic(errors.New("Then there's nothing we can do"))}func A() { defer fmt.Println("If it's more than 100 degrees...")}func main() { err := saveEarth() fmt.Println(err)}
Toute fonction différée sera exécutée après un appel de fonction mais avant une déclaration de retour. Ainsi, vous pouvez définir une variable retournée avant qu’une déclaration de retour ne soit exécutée. L’exemple de code ci-dessus compilerait comme:
If it's more than 100 degrees...Then there's nothing we can doProgram exited.
Error wrapping
Auparavant, l’error wrapping en Go était uniquement accessible via l’utilisation de paquets tels que pkg/errors
. Cependant, avec la dernière version de Go – version 1.13, le support du wrapping d’erreurs est présent. Selon les notes de version :
Une erreur
e
peut envelopper une autre erreurw
en fournissant une méthodeUnwrap
qui renvoiew
. Tante
quew
sont disponibles pour les programmes, permettant àe
de fournir un contexte supplémentaire àw
ou de le réinterpréter tout en permettant aux programmes de prendre des décisions basées surw
.
Pour créer des erreurs enveloppées, fmt.Errorf
a maintenant un verbe %w
et pour inspecter et déballer les erreurs, un couple de fonctions a été ajouté au paquet error
:
errors.Unwrap
: Cette fonction inspecte et expose essentiellement les erreurs sous-jacentes dans un programme. Elle renvoie le résultat de l’appel de la méthode Unwrap
sur Err
. Si le type de Err contient une méthode Unwrap
renvoyant une erreur. Sinon, Unwrap
renvoie nil
.
package errorstype Wrapper interface{ Unwrap() error}
Voici un exemple de mise en oeuvre de la méthode Unwrap
:
func(e*PathError)Unwrap()error{ return e.Err}
errors.Is
: Avec cette fonction, vous pouvez comparer une valeur d’erreur à la valeur sentinelle. Ce qui rend cette fonction différente de nos contrôles d’erreurs habituels est qu’au lieu de comparer la valeur sentinelle à une erreur, elle la compare à chaque erreur de la chaîne d’erreurs. Elle implémente également une méthode Is
sur une erreur afin qu’une erreur puisse s’afficher comme sentinelle même si elle n’est pas une valeur sentinelle.
func Is(err, target error) bool
Dans l’implémentation de base ci-dessus, Is
vérifie et rapporte si err
ou l’une des errors
de sa chaîne sont égales à target (valeur sentinelle).
errors.As
: Cette fonction fournit un moyen de cast vers un type d’erreur spécifique. Elle recherche la première erreur dans la chaîne d’erreurs qui correspond à la valeur sentinelle et, si elle est trouvée, définit la valeur sentinelle à cette valeur d’erreur et renvoie true
:
package mainimport ( "errors" "fmt" "os")func main() { if _, err := os.Open("non-existing"); err != nil { var pathError *os.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }}
Vous pouvez trouver ce code dans le code source de Go.
Résultat du compilateur:
Failed at path: non-existingProgram exited.
Une erreur correspond à la valeur sentinelle si la valeur concrète de l’erreur est assignable à la valeur pointée par la valeur sentinelle. As
panique si la valeur sentinelle n’est pas un pointeur non nul soit vers un type qui implémente error, soit vers un type d’interface quelconque. As
renvoie false si err
est nil
.
Summary
La communauté Go a fait des progrès impressionnants ces derniers temps avec le support de divers concepts de programmation et l’introduction de moyens encore plus concis et faciles de gérer les erreurs. Avez-vous des idées sur la façon de gérer ou de travailler avec les erreurs qui peuvent apparaître dans votre programme Go ? Faites-le moi savoir dans les commentaires ci-dessous.
Ressources:
La spécification du langage de programmation de Go sur l’assertion de type
La conférence de Marcel van Lohuizen à dotGo 2019 – Les valeurs d’erreur de Go 2 aujourd’hui
Go 1.13 release notes
LogRocket : Une visibilité totale sur vos applications web
LogRocket est une solution de surveillance des applications frontales qui vous permet de rejouer les problèmes comme s’ils se produisaient dans votre propre navigateur. Au lieu de deviner pourquoi les erreurs se produisent, ou de demander aux utilisateurs des captures d’écran et des vidages de journaux, LogRocket vous permet de rejouer la session pour comprendre rapidement ce qui a mal tourné. Il fonctionne parfaitement avec n’importe quelle application, quel que soit le framework, et dispose de plugins pour enregistrer le contexte supplémentaire de Redux, Vuex et @ngrx/store.
En plus d’enregistrer les actions et l’état de Redux, LogRocket enregistre les journaux de la console, les erreurs JavaScript, les stacktraces, les requêtes/réponses réseau avec les en-têtes + les corps, les métadonnées du navigateur et les journaux personnalisés. Il instrumente également le DOM pour enregistrer le HTML et le CSS de la page, recréant ainsi des vidéos au pixel près, même pour les applications monopages les plus complexes.
Essayez-le gratuitement.
.