Au cours de la dernière décennie, nous avons réussi à exploiter le fait que Go traite les
erreurs comme des valeurs . Bien que la bibliothèque standard ait un support minimal pour les erreurs: uniquement les fonctions
errors.New
et
fmt.Errorf
qui génèrent une erreur contenant uniquement un message - l'interface intégrée permet aux programmeurs Go d'ajouter des informations. Tout ce dont vous avez besoin est un type qui implémente la méthode
Error
:
type QueryError struct { Query string Err error } func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
Ces types d'erreurs se retrouvent dans toutes les langues et stockent une grande variété d'informations, des horodatages aux noms de fichiers et adresses de serveurs. Les erreurs de bas niveau qui fournissent un contexte supplémentaire sont souvent mentionnées.
Le modèle, lorsqu'une erreur en contient une autre, est rencontré si souvent dans Go qu'après une
discussion animée dans Go 1.13, son support explicite a été ajouté. Dans cet article, nous examinerons les ajouts à la bibliothèque standard qui fournissent le support mentionné: trois nouvelles fonctions dans le package d'erreurs et une nouvelle commande de formatage pour
fmt.Errorf
.
Avant de discuter des changements en détail, parlons de la manière dont les erreurs ont été étudiées et construites dans les versions précédentes du langage.
Erreurs avant Go 1.13
Recherche d'erreurs
Les erreurs dans Go sont des significations. Les programmes prennent des décisions basées sur ces valeurs de différentes manières. Le plus souvent, l'erreur est comparée à zéro pour voir si l'opération a échoué.
if err != nil {
Parfois, nous comparons l'erreur pour trouver la valeur de
contrôle et voir si une erreur spécifique s'est produite.
var ErrNotFound = errors.New("not found") if err == ErrNotFound {
La valeur d'erreur peut être de tout type satisfaisant l'interface d'erreur définie dans la langue. Un programme peut utiliser une instruction de type ou un commutateur de type pour afficher la valeur d'erreur d'un type plus spécifique.
type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + ": not found" } if e, ok := err.(*NotFoundError); ok {
Ajout d'informations
Souvent, une fonction transmet une erreur dans la pile d'appels, en y ajoutant des informations, par exemple, une brève description de ce qui s'est produit lorsque l'erreur s'est produite. C'est facile à faire, il suffit de construire une nouvelle erreur qui inclut le texte de l'erreur précédente:
if err != nil { return fmt.Errorf("decompress %v: %v", name, err) }
Lors de la création d'une nouvelle erreur à l'aide de
fmt.Errorf
nous
fmt.Errorf
tout sauf le texte de l'erreur d'origine. Comme nous l'avons vu dans l'exemple
QueryError
, vous devez parfois définir un nouveau type d'erreur qui contient l'erreur d'origine afin de la sauvegarder pour l'analyse à l'aide de code:
type QueryError struct { Query string Err error }
Les programmes peuvent regarder à l'intérieur de la
*QueryError
et prendre une décision basée sur l'erreur d'origine. Ceci est parfois appelé le déballage d'une erreur.
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
Le type
os.PathError
de la bibliothèque standard est un autre exemple de la façon dont une erreur en contient une autre.
Erreurs dans Go 1.13
Méthode de déballage
Dans Go 1.13, les packages de bibliothèques standard
errors
et
fmt
ont simplifié la
fmt
erreurs qui contiennent d'autres erreurs. La plus importante est la convention, pas la modification: une erreur contenant une autre erreur peut implémenter la méthode
Unwrap
, qui renvoie l'erreur d'origine. Si
e1.Unwrap()
renvoie
e2
, alors nous disons que
e1
emballe e2
et vous pouvez
décompresser e1
pour obtenir
e2
.
Selon cette convention, vous pouvez attribuer le type
QueryError
décrit ci-dessus à la méthode
QueryError
, qui renvoie l'erreur contenue dans celle-ci:
func (e *QueryError) Unwrap() error { return e.Err }
Le résultat du déballage de l'erreur peut également contenir la méthode
Unwrap
. La séquence d'erreurs obtenue par le déballage répété, nous appelons la
chaîne d'erreurs .
Recherche d'erreur avec Is et As
Dans Go 1.13, le package d'
errors
contient deux nouvelles fonctions pour rechercher les erreurs:
Is
et
As
.
La fonction
errors.Is
compare une erreur avec une valeur.
La fonction
As
vérifie si l'erreur est d'un type particulier.
Dans le cas le plus simple, la fonction
errors.Is
se comporte comme une comparaison avec une erreur de contrôle et la fonction
errors.As
se comporte comme une instruction de type. Cependant, lorsque vous travaillez avec des erreurs compressées, ces fonctions évaluent toutes les erreurs de la chaîne. Examinons l'exemple de
QueryError
ci-dessus pour examiner l'erreur d'origine:
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
En utilisant la fonction
errors.Is
,
errors.Is
pouvez écrire ceci:
if errors.Is(err, ErrPermission) {
Le package d'
errors
contient également une nouvelle fonction
Unwrap
qui renvoie le résultat de l'appel de la méthode
Unwrap
de l'erreur, ou renvoie nil si l'erreur n'a pas la méthode
Unwrap
. Il est généralement préférable d'utiliser des
errors.Is
des
errors.Is
ou des
errors.As
, car elles vous permettent d'examiner toute la chaîne en un seul appel.
Erreur de conditionnement avec% w
Comme je l'ai mentionné, il est normal d'utiliser la fonction
fmt.Errorf
pour ajouter des informations supplémentaires à l'erreur.
if err != nil { return fmt.Errorf("decompress %v: %v", name, err) }
Dans Go 1.13, la fonction
fmt.Errorf
prend en charge la nouvelle commande
%w
. Si c'est le cas, l'erreur renvoyée par
fmt.Errorf
contiendra la méthode
Unwrap
qui renvoie l'argument
%w
, qui devrait être une erreur. Dans tous les autres cas,
%w
identique à
%v
.
if err != nil {
L'emballage de l'erreur avec
%w
rend disponible pour les
errors.Is
et
errors.As
.
err := fmt.Errorf("access denied: %w", ErrPermission) ... if errors.Is(err, ErrPermission) ...
Quand emballer?
Lorsque vous ajoutez un contexte supplémentaire à l'erreur à l'aide de
fmt.Errorf
ou d'une implémentation de type personnalisé, vous devez décider si la nouvelle erreur contiendra l'original. Il n'y a pas de réponse unique à cela, tout dépend du contexte dans lequel la nouvelle erreur est créée. Pack pour montrer son appelant. Ne pas empaqueter l'erreur si cela conduit à la divulgation des détails d'implémentation.
Par exemple, imaginez une fonction
Parse
qui lit une structure de données complexe à partir de
io.Reader
. Si une erreur se produit, nous souhaitons connaître le numéro de la ligne et de la colonne où elle s'est produite. Si une erreur s'est produite lors de la lecture à partir de
io.Reader
, nous devrons l'emballer pour en trouver la raison. Étant donné que l'appelant a reçu la fonction
io.Reader
, il est logique d'afficher l'erreur qu'il a générée.
Autre cas: une fonction qui effectue plusieurs appels de base de données ne devrait probablement pas retourner une erreur dans laquelle le résultat de l'un de ces appels est compressé. Si la base de données utilisée par cette fonction fait partie de l'implémentation, la divulgation de ces erreurs violera l'abstraction. Par exemple, si la fonction
LookupUser
du package
pkg
utilise le package Go
database/sql
, elle peut rencontrer l'erreur
sql.ErrNoRows
. Si vous renvoyez une erreur à l'aide de
fmt.Errorf("accessing DB: %v", err)
, l'appelant ne peut pas regarder à l'intérieur et trouver
sql.ErrNoRows
. Mais si la fonction renvoie
fmt.Errorf("accessing DB: %w", err)
, alors l'appelant pourrait écrire:
err := pkg.LookupUser(...) if errors.Is(err, sql.ErrNoRows) …
Dans ce cas, la fonction doit toujours renvoyer
sql.ErrNoRows
si vous ne souhaitez pas interrompre les clients, même lorsque vous passez à un package avec une base de données différente. En d'autres termes, l'empaquetage fait une erreur dans votre API. Si vous ne souhaitez pas valider la prise en charge de cette erreur à l'avenir dans le cadre de l'API, ne la regroupez pas.
Il est important de se rappeler que, que vous l'emballiez ou non, l'erreur restera inchangée.
Une personne qui le comprendra aura les mêmes informations. La prise de décisions concernant l'emballage dépend de la nécessité ou non de fournir des informations supplémentaires aux
programmes afin qu'ils puissent prendre des décisions plus éclairées; ou si vous souhaitez masquer ces informations afin de maintenir le niveau d'abstraction.
Configuration des tests d'erreur à l'aide des méthodes Is et As
La fonction
errors.Is
vérifie chaque erreur de la chaîne par rapport à la valeur cible. Par défaut, une erreur correspond à cette valeur si elles sont équivalentes. De plus, une erreur dans la chaîne peut déclarer sa conformité à la valeur cible à l'aide de l'implémentation de
la méthode Is
.
Considérez l'erreur provoquée
par le package Upspin , qui compare l'erreur avec le modèle et évalue uniquement les champs non nuls:
type Error struct { Path string User string } func (e *Error) Is(target error) bool { t, ok := target.(*Error) if !ok { return false } return (e.Path == t.Path || t.Path == "") && (e.User == t.User || t.User == "") } if errors.Is(err, &Error{User: "someuser"}) {
La fonction
errors.As
conseille également la méthode
As
, le cas échéant.
Erreurs et API de package
Un package qui renvoie des erreurs (et la plupart des packages le font) doit décrire les propriétés de ces erreurs sur lesquelles un programmeur peut s'appuyer. Un package bien conçu évitera également de renvoyer des erreurs avec des propriétés sur lesquelles on ne peut pas compter.
Le plus simple est de dire si l'opération a réussi, en renvoyant respectivement la valeur nil ou non nil. Dans de nombreux cas, aucune autre information n'est requise.
Si vous avez besoin de la fonction pour renvoyer un état d'erreur identifiable, par exemple, «élément non trouvé», vous pouvez renvoyer une erreur dans laquelle la valeur du signal est compressée.
var ErrNotFound = errors.New("not found")
Il existe d'autres modèles pour fournir des erreurs que l'appelant peut examiner sémantiquement. Par exemple, renvoyez directement une valeur de contrôle, un type spécifique ou une valeur qui peut être analysée à l'aide d'une fonction prédicative.
Dans tous les cas, ne divulguez pas les détails internes à l'utilisateur. Comme mentionné dans le chapitre «Quand vaut-il la peine d'être empaqueté?», Si vous renvoyez une erreur à partir d'un autre package, convertissez-la afin de ne pas révéler l'erreur d'origine, sauf si vous avez l'intention de vous engager à renvoyer cette erreur spécifique à l'avenir.
f, err := os.Open(filename) if err != nil {
Si une fonction renvoie une erreur avec une valeur ou un type de signal compressé, ne retournez pas directement l'erreur d'origine.
var ErrPermission = errors.New("permission denied")
Conclusion
Bien que nous n'ayons discuté que de trois fonctions et d'une commande de formatage, nous espérons qu'elles contribueront grandement à améliorer la gestion des erreurs dans les programmes Go. Nous espérons que l'empaquetage dans le but de fournir un contexte supplémentaire deviendra une pratique normale, aidant les programmeurs à prendre de meilleures décisions et à trouver plus rapidement les bogues.
Comme Russ Cox l'a dit dans son
discours à GopherCon 2019 , sur le chemin de Go 2, nous expérimentons, simplifions et expédions. Et maintenant, après avoir expédié ces changements, nous avons entrepris de nouvelles expériences.