Gestion des erreurs en cours

Le code robuste doit réagir correctement à des circonstances imprévues, telles qu'une saisie utilisateur incorrecte, des connexions réseau défectueuses et des disques défaillants. Error handling est le processus d'identification du moment où votre programme est dans un état inattendu et de la prise de mesures pour enregistrer les informations de diagnostic pour un débogage ultérieur.

Contrairement à d'autres langages qui obligent les développeurs à gérer les erreurs avec une syntaxe spécialisée, les erreurs dans Go sont des valeurs de typeerror renvoyées par des fonctions comme toute autre valeur. Pour gérer les erreurs dans Go, nous devons examiner ces erreurs que les fonctions peuvent renvoyer, déterminer si une erreur s'est produite et prendre les mesures appropriées pour protéger les données et informer les utilisateurs ou les opérateurs que l'erreur est survenue.

Créer des erreurs

Avant de pouvoir gérer les erreurs, nous devons en créer d’abord. La bibliothèque standard fournit deux fonctions intégrées pour créer des erreurs:errors.New etfmt.Errorf. Ces deux fonctions vous permettent de spécifier un message d'erreur personnalisé que vous pourrez ensuite présenter à vos utilisateurs.

errors.New prend un seul argument - un message d'erreur sous forme de chaîne que vous pouvez personnaliser pour alerter vos utilisateurs de ce qui ne va pas.

Essayez d'exécuter l'exemple suivant pour voir une erreur créée parerrors.New imprimée sur la sortie standard:

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("barnacles")
    fmt.Println("Sammy says:", err)
}
OutputSammy says: barnacles

Nous avons utilisé la fonctionerrors.New de la bibliothèque standard pour créer un nouveau message d'erreur avec la chaîne"barnacles" comme message d'erreur. Nous avons suivi la convention ici en utilisant des minuscules pour le message d'erreur comme le suggère leGo Programming Language Style Guide.

Enfin, nous avons utilisé la fonctionfmt.Println pour combiner notre message d'erreur avec"Sammy says:".

La fonctionfmt.Errorf vous permet de créer dynamiquement un message d'erreur. Son premier argument est une chaîne contenant votre message d'erreur avec des valeurs d'espace réservé telles que%s pour une chaîne et%d pour un entier. fmt.Errorf interpole les arguments qui suivent cette chaîne de formatage dans ces espaces réservés dans l'ordre:

package main

import (
    "fmt"
    "time"
)

func main() {
    err := fmt.Errorf("error occurred at: %v", time.Now())
    fmt.Println("An error happened:", err)
}
OutputAn error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

Nous avons utilisé la fonctionfmt.Errorf pour créer un message d'erreur qui inclurait l'heure actuelle. La chaîne de mise en forme que nous avons fournie àfmt.Errorf contient la directive de mise en forme%v qui indique àfmt.Errorf d'utiliser le formatage par défaut pour le premier argument fourni après la chaîne de formatage. Cet argument sera l'heure actuelle, fournie par la fonctiontime.Now de la bibliothèque standard. De manière similaire à l'exemple précédent, nous combinons notre message d'erreur avec un préfixe court et imprimons le résultat sur la sortie standard à l'aide de la fonctionfmt.Println.

Gestion des erreurs

En règle générale, aucune erreur créée de la sorte ne sera utilisée immédiatement sans autre but, comme dans l'exemple précédent. En pratique, il est beaucoup plus courant de créer une erreur et de la renvoyer depuis une fonction en cas de problème. Les appelants de cette fonction utiliseront alors une instructionif pour voir si l'erreur était présente ounil - une valeur non initialisée.

Cet exemple suivant inclut une fonction qui renvoie toujours une erreur. Notez que lorsque vous exécutez le programme, il produit la même sortie que dans l'exemple précédent, même si une fonction renvoie l'erreur cette fois-ci. La déclaration d’une erreur dans un emplacement différent ne modifie pas le message de l’erreur.

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    err := boom()

    if err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
OutputAn error occurred: barnacles

Ici, nous définissons une fonction appeléeboom() qui renvoie un seulerror que nous construisons en utilisanterrors.New. Nous appelons ensuite cette fonction et capturons l'erreur avec la ligneerr := boom().
Une fois que nous affectons cette erreur, nous vérifions si elle était présente avec le conditionnelif err != nil. Ici, le conditionnel sera toujours évalué àtrue, puisque nous retournons toujours unerror deboom().

Ce ne sera pas toujours le cas. Il est donc recommandé d’avoir une gestion logique des cas où une erreur n’est pas présente (nil) et des cas où l’erreur est présente. Lorsque l'erreur est présente, nous utilisonsfmt.Println pour afficher notre erreur avec un préfixe comme nous l'avons fait dans les exemples précédents. Enfin, nous utilisons une instructionreturn pour ignorer l'exécution defmt.Println("Anchors away!"), car cela ne devrait s'exécuter que lorsqu'aucune erreur ne s'est produite.

[.note] #Note: La constructionif err != nil montrée dans le dernier exemple est le cheval de bataille de la gestion des erreurs dans le langage de programmation Go. Partout où une fonction peut produire une erreur, il est important d'utiliser une instructionif pour vérifier si une erreur s'est produite. De cette façon, le code Go idiomatique a naturellement sa logique“happy path” au premier niveau de retrait, et toute la logique de «triste chemin» au deuxième niveau de retrait.
#

Si les instructions ont une clause d'affectation facultative qui peut être utilisée pour aider à condenser l'appel d'une fonction et la gestion de ses erreurs.

Exécutez le programme suivant pour voir la même sortie que notre exemple précédent, mais cette fois en utilisant une instruction composéeif pour réduire un peu passe-partout:

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    if err := boom(); err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
OutputAn error occurred: barnacles

Comme précédemment, nous avons une fonction,boom(), qui renvoie toujours une erreur. Nous affectons l'erreur renvoyée deboom() àerr comme première partie de l'instructionif. Dans la deuxième partie de l'instructionif, après le point-virgule, cette variableerr est alors disponible. Nous vérifions si l'erreur était présente et imprimons notre erreur avec une chaîne de préfixe courte, comme nous l'avons fait précédemment.

Dans cette section, nous avons appris à gérer des fonctions ne renvoyant qu’une erreur. Ces fonctions sont courantes, mais il est également important de pouvoir gérer les erreurs provenant de fonctions pouvant renvoyer plusieurs valeurs.

Renvoi d'erreurs à côté de valeurs

Les fonctions qui renvoient une seule valeur d'erreur sont souvent celles qui effectuent des modifications avec état, telles que l'insertion de lignes dans une base de données. Il est également courant d’écrire des fonctions qui renvoient une valeur si elles aboutissent avec une erreur potentielle en cas d’échec de cette fonction. Go permet aux fonctions de renvoyer plusieurs résultats, ce qui peut être utilisé pour renvoyer simultanément une valeur et un type d'erreur.

Pour créer une fonction renvoyant plusieurs valeurs, nous répertorions les types de chaque valeur renvoyée entre parenthèses dans la signature de la fonction. Par exemple, une fonctioncapitalize qui renvoie unstring et unerror serait déclarée en utilisantfunc capitalize(name string) (string, error) {}. La partie(string, error) indique au compilateur Go que cette fonction renverra unstring et unerror, dans cet ordre.

Exécutez le programme suivant pour voir la sortie d'une fonction qui renvoie à la fois unstring et unerror:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    name, err := capitalize("sammy")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }

    fmt.Println("Capitalized name:", name)
}
OutputCapitalized name: SAMMY

Nous définissonscapitalize() comme une fonction qui prend une chaîne (le nom à mettre en majuscule) et retourne une chaîne et une valeur d'erreur. Dansmain(), nous appelonscapitalize() et assignons les deux valeurs renvoyées par la fonction aux variablesname eterr en les séparant par des virgules sur le côté gauche du Opérateur:=. Après cela, nous effectuons notre vérification deif err != nil comme dans les exemples précédents, en imprimant l'erreur sur la sortie standard en utilisantfmt.Println si l'erreur était présente. Si aucune erreur n'était présente, nous imprimonsCapitalized name: SAMMY.

Essayez de remplacer la chaîne"sammy" dename, err := capitalize("sammy") par la chaîne vide("") et vous recevrez l’erreurCould not capitalize: no name provided à la place.

La fonctioncapitalize renverra une erreur lorsque les appelants de la fonction fournissent une chaîne vide pour le paramètrename. Lorsque le paramètrename n'est pas la chaîne vide,capitalize() utilisestrings.ToTitle pour mettre en majuscule le paramètrename et renvoienil pour la valeur d'erreur.

Cet exemple suit certaines conventions subtiles qui sont typiques du code Go, mais qui ne sont pas appliquées par le compilateur Go. Lorsqu'une fonction renvoie plusieurs valeurs, y compris une erreur, la convention demande que nous retournions leserror comme dernier élément. Lors du retour d'unerror à partir d'une fonction avec plusieurs valeurs de retour, le code Go idiomatique définira également chaque valeur sans erreur sur unzero value. Les valeurs nulles sont, par exemple, une chaîne vide pour les chaînes,0 pour les entiers, une structure vide pour les types struct etnil pour les types interface et pointeur, pour n'en nommer que quelques-uns. Nous couvrons les valeurs nulles plus en détail dans nostutorial on variables and constants.

Réduire le passe-partout

Le respect de ces conventions peut devenir fastidieux dans les situations où il existe de nombreuses valeurs à renvoyer d'une fonction. Nous pouvons utiliser unanonymous function pour aider à réduire le passe-partout. Les fonctions anonymes sont des procédures assignées à des variables. Contrairement aux fonctions que nous avons définies dans les exemples précédents, elles ne sont disponibles que dans les fonctions où vous les déclarez, ce qui les rend parfaites pour agir comme de courts morceaux de logique d'aide réutilisable.

Le programme suivant modifie le dernier exemple pour inclure la longueur du nom que nous capitalisons. Comme il a trois valeurs à renvoyer, les erreurs de traitement pourraient devenir encombrantes sans une fonction anonyme pour nous aider:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, int, error) {
    handle := func(err error) (string, int, error) {
        return "", 0, err
    }

    if name == "" {
        return handle(errors.New("no name provided"))
    }

    return strings.ToTitle(name), len(name), nil
}

func main() {
    name, size, err := capitalize("sammy")
    if err != nil {
        fmt.Println("An error occurred:", err)
    }

    fmt.Printf("Capitalized name: %s, length: %d", name, size)
}
OutputCapitalized name: SAMMY, length: 5

Dansmain(), nous capturons maintenant les trois arguments renvoyés parcapitalize en tant quename,size eterr, respectivement. Nous vérifions ensuite sicapitalize a renvoyé unerror en vérifiant si la variableerr n'était pas égale ànil. Ceci est important à faire avant d'essayer d'utiliser l'une des autres valeurs renvoyées parcapitalize, car la fonction anonyme,handle, pourrait les mettre à zéro. Comme aucune erreur ne s'est produite car nous avons fourni la chaîne"sammy", nous imprimons le nom en majuscule et sa longueur.

Une fois de plus, vous pouvez essayer de remplacer"sammy" par la chaîne vide("") pour voir le cas d'erreur imprimé (An error occurred: no name provided).

Danscapitalize, nous définissons la variablehandle comme une fonction anonyme. Il prend une seule erreur et renvoie des valeurs identiques dans le même ordre que les valeurs de retour decapitalize. handle définit ces valeurs sur des valeurs nulles et transmet leserror passés comme argument comme valeur de retour finale. En utilisant cela, nous pouvons ensuite retourner toutes les erreurs rencontrées danscapitalize en utilisant l'instructionreturn devant l'appel àhandle avec leerror comme paramètre.

N'oubliez pas quecapitalize doit renvoyer trois valeurs en permanence, car c'est ainsi que nous avons défini la fonction. Parfois, nous ne voulons pas traiter de toutes les valeurs pouvant être renvoyées par une fonction. Heureusement, nous avons une certaine flexibilité dans la manière dont nous pouvons utiliser ces valeurs du côté des affectations.

Gestion des erreurs à partir de fonctions multi-retours

Lorsqu'une fonction renvoie plusieurs valeurs, Go nous oblige à les affecter à une variable. Dans le dernier exemple, nous faisons cela en fournissant des noms pour les deux valeurs renvoyées par la fonctioncapitalize. Ces noms doivent être séparés par des virgules et apparaître sur le côté gauche de l'opérateur:=. La première valeur renvoyée parcapitalize sera affectée à la variablename, et la seconde valeur (leserror) sera affectée à la variableerr. Parfois, nous ne sommes intéressés que par la valeur d'erreur. Vous pouvez ignorer toutes les valeurs indésirables renvoyées par les fonctions en utilisant le nom de variable spécial_.

Dans le programme suivant, nous avons modifié notre premier exemple impliquant la fonctioncapitalize pour produire une erreur en passant la chaîne vide(""). Essayez d'exécuter ce programme pour voir comment nous pouvons examiner uniquement l'erreur en ignorant la première valeur renvoyée avec la variable_:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    _, err := capitalize("")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }
    fmt.Println("Success!")
}
OutputCould not capitalize: no name provided

Dans la fonctionmain() cette fois, nous attribuons le nom en majuscule (lestring retourné en premier) à la variable de soulignement (_). En même temps, nous affectons leserror renvoyés parcapitalize à la variableerr. Nous vérifions ensuite si l'erreur était présente dans le conditionnelif err != nil. Puisque nous avons codé en dur une chaîne vide comme argument decapitalize dans la ligne_, err := capitalize(""), ce conditionnel sera toujours évalué àtrue. Cela produit la sortie"Could not capitalize: no name provided" imprimée par l'appel à la fonctionfmt.Println dans le corps de l'instructionif. Lesreturn après cela sauteront lesfmt.Println("Success!").

Conclusion

Nous avons vu de nombreuses façons de créer des erreurs en utilisant la bibliothèque standard et de créer des fonctions qui renvoient les erreurs de manière idiomatique. Dans ce didacticiel, nous avons réussi à créer diverses erreurs à l'aide des fonctions de bibliothèque standarderrors.New etfmt.Errorf. Dans les prochains tutoriels, nous verrons comment créer nos propres types d'erreur personnalisés pour transmettre des informations plus riches aux utilisateurs.