Apprendre à gérer les erreurs dans Go

Effectué

Pendant que vous écrivez vos programmes, vous devez prendre en compte les différentes façons dont vos programmes peuvent échouer, et vous devez gérer les défaillances. Vos utilisateurs n’ont pas besoin de voir une erreur de trace de pile longue et prêtant à confusion. Il est préférable qu’ils voient des informations significatives sur le problème rencontré. Comme vous l’avez constaté, Go offre des fonctions intégrées telles que panic et recover pour gérer les exceptions, ou un comportement inattendu, dans vos programmes. Toutefois, les erreurs sont des défaillances connues et vos programmes devraient être conçus pour les gérer.

L’approche de la gestion des erreurs de Go est simplement un mécanisme de flux de contrôle où seules des instructions if et return sont nécessaires. Par exemple, lorsque vous appelez une fonction pour obtenir des informations à partir d’un objet employee, vous souhaiterez peut-être savoir si l’employé existe. Le mode de gestion strict de Go pour ce type d’erreur attendue devrait ressembler à ceci :

employee, err := getInformation(1000)
if err != nil {
    // Something is wrong. Do something.
}

Notez comment la fonction getInformation retourne le struct employee et également une erreur comme deuxième valeur. L’erreur peut être nil. L’erreur nil indique la réussite. S’il ne s’agit pas d’une erreur nil, cela signifie un échec. Une erreur non-nil accompagne un message d’erreur que vous pouvez imprimer ou, de préférence, journaliser. Voici comment gérer les erreurs dans Go. Nous allons aborder quelques autres stratégies dans la section suivante.

Vous remarquerez probablement que la gestion des erreurs dans Go vous demande de faire plus attention à la façon dont vous signalez et gérez une erreur. C’est bien là le point essentiel. Examinons d’autres exemples qui vous aideront à mieux comprendre l’approche de la gestion des erreurs de Go.

Nous utiliserons l’extrait de code que nous avons utilisé pour les structs afin de tester diverses stratégies de gestion des erreurs :

package main

import (
    "fmt"
    "os"
)

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

func main() {
    employee, err := getInformation(1001)
    if err != nil {
        // Something is wrong. Do something.
    } else {
        fmt.Print(employee)
    }
}

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    return employee, err
}

func apiCallEmployee(id int) (*Employee, error) {
    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}

À partir de là, nous allons nous concentrer sur la modification des fonctions getInformation, apiCallEmployee et main pour montrer comment gérer les erreurs.

Stratégies de gestion des erreurs

Quand une fonction retourne une erreur, il s’agit généralement de la dernière valeur de retour. Il incombe à l’appelant de vérifier s’il existe une erreur et de la gérer, comme vous l’avez vu dans la section précédente. Par conséquent, une stratégie courante consiste à continuer à utiliser ce modèle pour propager l’erreur dans une sous-routine. Par exemple, une sous-routine (comme getInformation dans l’exemple précédent) peut retourner l’erreur à l’appelant sans rien faire d’autre, comme suit :

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, err // Simply return the error to the caller.
    }
    return employee, nil
}

Vous pouvez également inclure des informations supplémentaires avant de propager l’erreur. À cet effet, vous pouvez utiliser la fonction fmt.Errorf(), qui est similaire à ce que nous avons vu précédemment, mais elle retourne une erreur. Par exemple, vous pouvez ajouter davantage de contexte à l’erreur et retourner quand même l’erreur d’origine, comme suit :

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
    }
    return employee, nil
}

Une autre stratégie consiste à exécuter une logique de nouvelle tentative lorsque les erreurs sont temporaires. Par exemple, vous pouvez utiliser une stratégie de nouvelle tentative pour appeler une fonction trois fois et attendre deux secondes, comme suit :

func getInformation(id int) (*Employee, error) {
    for tries := 0; tries < 3; tries++ {
        employee, err := apiCallEmployee(1000)
        if err == nil {
            return employee, nil
        }

        fmt.Println("Server is not responding, retrying ...")
        time.Sleep(time.Second * 2)
    }

    return nil, fmt.Errorf("server has failed to respond to get the employee information")
}

Enfin, au lieu d’imprimer des erreurs dans la console, vous pouvez journaliser les erreurs et cacher les détails d’implémentation aux utilisateurs finaux. Nous aborderons la journalisation dans le module suivant. Pour l’instant, voyons comment vous pouvez créer et utiliser des erreurs personnalisées.

Créer des erreurs réutilisables

Parfois, le nombre de messages d’erreur augmente et vous souhaitez maintenir l’ordre. Vous pouvez également créer une bibliothèque pour les messages d’erreur courants que vous souhaitez réutiliser. Dans Go, vous pouvez utiliser la fonction errors.New() pour créer des erreurs et les réutiliser dans plusieurs parties, comme suit :

var ErrNotFound = errors.New("Employee not found!")

func getInformation(id int) (*Employee, error) {
    if id != 1001 {
        return nil, ErrNotFound
    }

    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}

Le code de la fonction getInformation semble préférable et, si vous avez besoin de modifier le message d’erreur, vous le faites à un seul endroit. Notez également que la convention consiste à inclure le préfixe Err pour les variables d’erreur.

Enfin, lorsque vous avez une variable d’erreur, vous pouvez être plus précis quand vous gérez une erreur dans une fonction d’appelant. La fonction errors.Is() vous permet de comparer le type d’erreur que vous obtenez, comme suit :

employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
    fmt.Printf("NOT FOUND: %v\n", err)
} else {
    fmt.Print(employee)
}

Lorsque vous gérez des erreurs dans Go, voici quelques pratiques recommandées à garder à l’esprit :

  • Recherchez toujours les erreurs, même si vous ne les attendez pas. Gérez-les ensuite correctement pour éviter d’exposer des informations inutiles aux utilisateurs finaux.
  • Incluez un préfixe dans un message d’erreur afin de connaître l’origine de l’erreur. Par exemple, vous pouvez inclure le nom du package et de la fonction.
  • Créez autant de variables d’erreur réutilisables que vous le pouvez.
  • Comprenez la différence entre le recours au retour d’erreurs et la crise de panique. Paniquez quand vous ne pouvez rien faire d’autre. Par exemple, si une dépendance n’est pas prête, il n’est pas judicieux de faire fonctionner le programme (sauf si vous souhaitez exécuter un comportement par défaut).
  • Journalisez les erreurs avec autant de détails que possible (nous allons voir comment dans la section suivante) et imprimez les erreurs qu’un utilisateur final peut comprendre.