Gérer les bogues dans Go 1.13


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 { // something went wrong } 

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 { // something wasn't found } 

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 { // e.Name wasn't found } 

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 { // query failed because of a permission problem } 

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.

 // Similar to: // if err == ErrNotFound { … } if errors.Is(err, ErrNotFound) { // something wasn't found } 

La fonction As vérifie si l'erreur est d'un type particulier.

 // Similar to: // if e, ok := err.(*QueryError); ok { … } var e *QueryError if errors.As(err, &e) { // err is a *QueryError, and e is set to the error's value } 

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 { // query failed because of a permission problem } 

En utilisant la fonction errors.Is , errors.Is pouvez écrire ceci:

 if errors.Is(err, ErrPermission) { // err, or some error that it wraps, is a permission problem } 

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 { // Return an error which unwraps to err. return fmt.Errorf("decompress %v: %w", name, err) } 

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"}) { // err's User field is "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") // FetchItem returns the named item. // // If no item with the name exists, FetchItem returns an error // wrapping ErrNotFound. func FetchItem(name string) (*Item, error) { if itemNotFound(name) { return nil, fmt.Errorf("%q: %w", name, ErrNotFound) } // ... } 

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 { // The *os.PathError returned by os.Open is an internal detail. // To avoid exposing it to the caller, repackage it as a new // error with the same text. We use the %v formatting verb, since // %w would permit the caller to unwrap the original *os.PathError. return fmt.Errorf("%v", err) } 

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") // DoSomething returns an error wrapping ErrPermission if the user // does not have permission to do something. func DoSomething() { if !userHasPermission() { // If we return ErrPermission directly, callers might come // to depend on the exact error value, writing code like this: // // if err := pkg.DoSomething(); err == pkg.ErrPermission { … } // // This will cause problems if we want to add additional // context to the error in the future. To avoid this, we // return an error wrapping the sentinel so that users must // always unwrap it: // // if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... } return fmt.Errorf("%w", ErrPermission) } // ... } 

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.

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


All Articles