Spécification de gestion des erreurs en Golang
Types d'erreur
Il existe quelques options pour déclarer les erreurs. Avant de choisir l'option qui convient le mieux à votre cas d'utilisation, prenez en compte les éléments suivants :
- Est-ce que l'appelant doit faire correspondre l'erreur pour la gérer ? Si c'est le cas, nous devons prendre en charge les fonctions
errors.Is
ouerrors.As
en déclarant des variables d'erreur de niveau supérieur ou des types personnalisés. - Le message d'erreur est-il une chaîne statique ou une chaîne dynamique nécessitant des informations contextuelles ? Pour les chaînes statiques, nous pouvons utiliser
errors.New
, mais pour les chaînes dynamiques, nous devons utiliserfmt.Errorf
ou un type d'erreur personnalisé. - Transmettons-nous de nouvelles erreurs renvoyées par des fonctions en aval ? Si c'est le cas, reportez-vous à la section sur l'encapsulation des erreurs.
Correspondance d'erreur ? | Message d'erreur | Conseils |
---|---|---|
Non | statique | errors.New |
Non | dynamique | fmt.Errorf |
Oui | statique | var de niveau supérieur avec errors.New |
Oui | dynamique | type error personnalisé |
Par exemple, utilisez errors.New
pour représenter les erreurs avec des chaînes statiques. Si l'appelant doit faire correspondre et gérer cette erreur, exportez-la en tant que variable pour prendre en charge la correspondance avec errors.Is
.
Aucune correspondance d'erreur
// package foo
func Open() error {
return errors.New("could not open")
}
// package bar
if err := foo.Open(); err != nil {
// Impossible de gérer l'erreur.
panic("erreur inconnue")
}
Correspondance d'erreur
// package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// gérer l'erreur
} else {
panic("erreur inconnue")
}
}
Pour les erreurs avec des chaînes dynamiques, utilisez fmt.Errorf
si l'appelant n'a pas besoin de faire correspondre l'erreur. Si l'appelant a effectivement besoin de la faire correspondre, utilisez alors une erreur personnalisée.
Aucune correspondance d'erreur
// package foo
func Open(file string) error {
return fmt.Errorf("fichier %q non trouvé", file)
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
// Impossible de gérer l'erreur.
panic("erreur inconnue")
}
Correspondance d'erreur
// package foo
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("fichier %q non trouvé", e.File)
}
func Open(file string) error {
return &NotFoundError{File: file}
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
var notFound *NotFoundError
if errors.As(err, ¬Found) {
// gérer l'erreur
} else {
panic("erreur inconnue")
}
}
Notez que si vous exportez des variables ou des types d'erreur d'un package, ils deviennent une partie de l'API publique du package.
Enrobage des erreurs
Lorsqu'une erreur se produit lors de l'appel d'une autre méthode, il existe généralement trois façons de la gérer :
- Retourner l'erreur d'origine telle quelle.
- Utiliser
fmt.Errorf
avec%w
pour ajouter du contexte à l'erreur, puis la retourner. - Utiliser
fmt.Errorf
avec%v
pour ajouter du contexte à l'erreur, puis la retourner.
S'il n'y a pas de contexte supplémentaire à ajouter, retournez l'erreur d'origine telle quelle. Cela préservera le type d'erreur d'origine et son message. C'est particulièrement adapté lorsque le message d'erreur sous-jacent contient suffisamment d'informations pour retracer l'origine de l'erreur.
Sinon, ajoutez autant de contexte que possible au message d'erreur afin d'éviter les erreurs ambiguës telles que "refus de connexion". Vous recevrez alors des erreurs plus utiles, telles que "appel du service foo : refus de connexion".
Utilisez fmt.Errorf
pour ajouter du contexte à vos erreurs et choisissez entre les verbes %w
ou %v
en fonction de la capacité du destinataire à identifier et extraire la cause première.
- Utilisez
%w
si le destinataire doit avoir accès à l'erreur sous-jacente. C'est une bonne option par défaut pour la plupart des erreurs d'enrobage, mais gardez à l'esprit que le destinataire peut commencer à compter sur ce comportement. Par conséquent, pour les erreurs d'enrobage qui sont des variables ou des types connus, enregistrez-les et testez-les dans le cadre du contrat de la fonction. - Utilisez
%v
pour masquer l'erreur sous-jacente. Le destinataire ne pourra pas l'identifier, mais vous pourrez passer à%w
à l'avenir si nécessaire.
Lorsque vous ajoutez du contexte à l'erreur retournée, évitez d'utiliser des phrases telles que "échec de" pour garder le contexte concis. Lorsque l'erreur se propage dans la pile, elle sera empilée couche par couche :
Non recommandé :
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %w", err)
}
// échec de x : échec de y : échec de création d'un nouveau magasin : l'erreur
Recommandé :
s, err := store.New()
if err != nil {
return fmt.Errorf(
"nouveau magasin : %w", err)
}
// x : y : nouveau magasin : l'erreur
Cependant, une fois l'erreur envoyée à un autre système, il doit être clair que le message est une erreur (par exemple, une étiquette "err" ou un préfixe "Failed" dans les journaux).
Nom incorrect
Pour les valeurs d'erreur stockées en tant que variables globales, utilisez le préfixe Err
ou err
en fonction de leur exportation. Veuillez vous référer aux directives. Pour les constantes et variables de niveau supérieur non exportées, utilisez un tiret bas (_) comme préfixe.
var (
// Exportez les deux erreurs suivantes afin que les utilisateurs de ce package puissent les identifier avec errors.Is.
ErrBrokenLink = errors.New("le lien est rompu")
ErrCouldNotOpen = errors.New("impossible d'ouvrir")
// Cette erreur n'est pas exportée car nous ne voulons pas qu'elle fasse partie de notre API publique. Nous pouvons toujours l'utiliser à l'intérieur du package avec errors.
errNotFound = errors.New("introuvable")
)
Pour les types d'erreurs personnalisés, utilisez le suffixe Error
.
// De même, cette erreur est exportée pour que les utilisateurs de ce package puissent l'identifier avec errors.As.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("fichier %q non trouvé", e.File)
}
// Cette erreur n'est pas exportée car nous ne voulons pas qu'elle fasse partie de l'API publique. Nous pouvons toujours l'utiliser à l'intérieur d'un package avec errors.As.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("résoudre %q", e.Path)
}
Gestion des erreurs
Lorsque l'appelant reçoit une erreur du destinataire, il peut gérer l'erreur de différentes manières en fonction de sa compréhension de l'erreur.
Cela inclut, sans s'y limiter :
- Faire correspondre l'erreur avec
errors.Is
ouerrors.As
si le destinataire a convenu d'une définition d'erreur spécifique, et gérer les bifurcations de différentes manières - Enregistrer l'erreur et dégrader gracieusement si l'erreur est récupérable
- Retourner une erreur bien définie si elle représente une condition d'échec spécifique au domaine
- Retourner l'erreur, qu'elle soit enveloppée ou brute
Indépendamment de la manière dont l'appelant gère l'erreur, il doit généralement ne traiter chaque erreur qu'une seule fois. Par exemple, l'appelant ne doit pas enregistrer l'erreur puis la retourner, car son appelant pourrait également gérer l'erreur.
Par exemple, considérez les scénarios suivants :
Mauvais : Enregistrer l'erreur et la retourner
D'autres appelants plus haut dans la pile pourraient prendre des actions similaires sur cette erreur. Cela créerait beaucoup de bruit dans les journaux de l'application avec peu de bénéfice.
u, err := getUser(id)
if err != nil {
// MAUVAIS: Voir la description
log.Printf("Impossible d'obtenir l'utilisateur %q: %v", id, err)
return err
}
Bien : Envelopper l'erreur et la retourner
Les erreurs plus haut dans la pile géreront cette erreur. En utilisant %w
, cela garantit qu'elles peuvent faire correspondre l'erreur avec errors.Is
ou errors.As
si nécessaire.
u, err := getUser(id)
if err != nil {
return fmt.Errorf("obtenir l'utilisateur %q: %w", id, err)
}
Bien : Enregistrer l'erreur et dégrader gracieusement
Si l'opération n'est pas absolument nécessaire, nous pouvons fournir une dégradation gracieuse en récupérant l'erreur sans interrompre l'expérience.
if err := emitMetrics(); err != nil {
// L'échec d'écrire des métriques ne devrait pas
// interrompre l'application.
log.Printf("Impossible d'émettre des métriques : %v", err)
}
Bien : Faire correspondre l'erreur et dégrader gracieusement de manière appropriée
Si le destinataire a défini une erreur spécifique dans son accord et que l'échec est récupérable, faire correspondre ce cas d'erreur et dégrader gracieusement. Pour tous les autres cas, envelopper l'erreur et la retourner. Les erreurs plus haut dans la pile géreront les autres erreurs.
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// L'utilisateur n'existe pas. Utiliser UTC.
tz = time.UTC
} else {
return fmt.Errorf("obtenir l'utilisateur %q: %w", id, err)
}
}
Gestion des échecs d'assertion
Les assertions de type provoqueront une panique avec une seule valeur de retour en cas de détection incorrecte du type. Par conséquent, utilisez toujours l'idiome "virgule, ok".
Non recommandé :
t := i.(string)
Recommandé :
t, ok := i.(string)
if !ok {
// Gérer l'erreur de manière gracieuse
}
Évitez d'utiliser panic
Le code s'exécutant dans un environnement de production doit éviter les panic. Les panic sont une source principale de défaillances en cascade. En cas d'erreur, la fonction doit renvoyer l'erreur et permettre à l'appelant de décider comment la gérer.
Non recommandé :
func run(args []string) {
if len(args) == 0 {
panic("un argument est requis")
}
// ...
}
func main() {
run(os.Args[1:])
}
Recommandé :
func run(args []string) error {
if len(args) == 0 {
return errors.New("un argument est requis")
}
// ...
return nil
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Panic/recover n'est pas une stratégie de gestion des erreurs. Il ne doit paniquer que lorsqu'un événement irrécupérable (par exemple, une référence nulle) se produit. Une exception est lors de l'initialisation du programme : les situations qui provoqueraient un panic doivent être gérées lors du démarrage du programme.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
Même dans le code de test, il est préférable d'utiliser t.Fatal
ou t.FailNow
au lieu de panic pour s'assurer que les échecs sont signalés.
Non recommandé :
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
panic("échec de la mise en place du test")
}
Recommandé :
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal("échec de la mise en place du test")
}