Créer des erreurs personnalisées en Go

introduction

Go propose deux méthodes pour créer des erreurs dans la bibliothèque standard,errors.New and fmt.Errorf. Lorsque vous communiquez des informations d'erreur plus complexes à vos utilisateurs ou à votre futur utilisateur lors du débogage, ces deux mécanismes ne suffisent parfois pas pour capturer et signaler correctement ce qui s'est passé. Pour transmettre ces informations d'erreur plus complexes et obtenir plus de fonctionnalités, nous pouvons implémenter le type d'interface de bibliothèque standard,error.

La syntaxe pour cela serait la suivante:

type error interface {
  Error() string
}

Le packagebuiltin définiterror comme une interface avec une seule méthodeError() qui renvoie un message d'erreur sous forme de chaîne. En implémentant cette méthode, nous pouvons transformer n'importe quel type défini en une erreur de notre part.

Essayons d'exécuter l'exemple suivant pour voir une implémentation de l'interfaceerror:

package main

import (
    "fmt"
    "os"
)

type MyError struct{}

func (m *MyError) Error() string {
    return "boom"
}

func sayHello() (string, error) {
    return "", &MyError{}
}

func main() {
    s, err := sayHello()
    if err != nil {
        fmt.Println("unexpected error: err:", err)
        os.Exit(1)
    }
    fmt.Println("The string:", s)
}

Nous verrons le résultat suivant:

Outputunexpected error: err: boom
exit status 1

Ici, nous avons créé un nouveau type de structure vide,MyError, et défini la méthodeError() dessus. La méthodeError() renvoie la chaîne"boom".

Dansmain(), nous appelons la fonctionsayHello qui renvoie une chaîne vide et une nouvelle instance deMyError. PuisquesayHello renverra toujours une erreur, l'appel defmt.Println dans le corps de l'instruction if dansmain() s'exécutera toujours. Nous utilisons ensuitefmt.Println pour afficher la courte chaîne de préfixe"unexpected error:" avec l'instance deMyError contenue dans la variableerr.

Notez que nous n'avons pas besoin d'appeler directementError(), puisque le packagefmt est capable de détecter automatiquement qu'il s'agit d'une implémentation deerror. Il appelleError()transparently pour obtenir la chaîne"boom" et la concatène avec la chaîne de préfixe"unexpected error: err:".

Collecte d'informations détaillées dans une erreur personnalisée

Parfois, une erreur personnalisée est le moyen le plus propre de capturer des informations d'erreur détaillées. Par exemple, supposons que nous souhaitons capturer le code d’état des erreurs générées par une requête HTTP; exécutez le programme suivant pour voir une implémentation deerror qui nous permet de capturer proprement ces informations:

package main

import (
    "errors"
    "fmt"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("success!")
}

Nous verrons la sortie suivante:

Outputstatus 503: err unavailable
exit status 1

Dans cet exemple, nous créons une nouvelle instance deRequestError et fournissons le code d'état et une erreur en utilisant la fonctionerrors.New de la bibliothèque standard. Nous imprimons ensuite ceci en utilisantfmt.Println comme dans les exemples précédents.

Dans la méthodeError() deRequestError, nous utilisons la fonctionfmt.Sprintf pour construire une chaîne en utilisant les informations fournies lors de la création de l'erreur.

Assertions de type et erreurs personnalisées

L'interface deerror n'expose qu'une seule méthode, mais nous pouvons avoir besoin d'accéder aux autres méthodes des implémentations deerror pour gérer correctement une erreur. Par exemple, nous pouvons avoir plusieurs implémentations personnalisées deerror qui sont temporaires et peuvent être réessayées - dénotées par la présence d'une méthodeTemporary().

Les interfaces fournissent une vue étroite de l'ensemble plus large des méthodes fournies par les types, nous devons donc utiliser untype assertion pour changer les méthodes que la vue affiche, ou pour la supprimer complètement.

L'exemple suivant augmente lesRequestError indiqués précédemment pour avoir une méthodeTemporary() qui indiquera si les appelants doivent réessayer la demande:

package main

import (
    "errors"
    "fmt"
    "net/http"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return r.Err.Error()
}

func (r *RequestError) Temporary() bool {
    return r.StatusCode == http.StatusServiceUnavailable // 503
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        re, ok := err.(*RequestError)
        if ok {
            if re.Temporary() {
                fmt.Println("This request can be tried again")
            } else {
                fmt.Println("This request cannot be tried again")
            }
        }
        os.Exit(1)
    }

    fmt.Println("success!")
}

Nous verrons la sortie suivante:

Outputunavailable
This request can be tried again
exit status 1

Dansmain(), nous appelonsdoRequest() qui nous renvoie une interfaceerror. Nous imprimons d'abord le message d'erreur renvoyé par la méthodeError(). Ensuite, nous essayons d'exposer toutes les méthodes deRequestError en utilisant l'assertion de typere, ok := err.(*RequestError). Si l'assertion de type réussit, nous utilisons alors la méthodeTemporary() pour voir si cette erreur est une erreur temporaire. Puisque leStatusCode défini pardoRequest() est503, ce qui correspond àhttp.StatusServiceUnavailable, cela renvoietrue et entraîne l'impression de"This request can be tried again". En pratique, nous ferions plutôt une autre demande plutôt que d’imprimer un message.

Erreurs d'emballage

Généralement, une erreur est générée à partir de quelque chose en dehors de votre programme, tel que: une base de données, une connexion réseau, etc. Les messages d'erreur fournis à partir de ces erreurs n'aident personne à trouver l'origine de l'erreur. Des erreurs d’emballage avec des informations supplémentaires au début d’un message d’erreur fourniraient le contexte nécessaire au débogage.

L'exemple suivant montre comment nous pouvons attacher des informations contextuelles à unerror autrement crypté renvoyé par une autre fonction:

package main

import (
    "errors"
    "fmt"
)

type WrappedError struct {
    Context string
    Err     error
}

func (w *WrappedError) Error() string {
    return fmt.Sprintf("%s: %v", w.Context, w.Err)
}

func Wrap(err error, info string) *WrappedError {
    return &WrappedError{
        Context: info,
        Err:     err,
    }
}

func main() {
    err := errors.New("boom!")
    err = Wrap(err, "main")

    fmt.Println(err)
}

Nous verrons la sortie suivante:

Outputmain: boom!

WrappedError est une structure avec deux champs: un message de contexte en tant questring, et unerror sur lequel ceWrappedError fournit plus d'informations. Lorsque la méthodeError() est invoquée, nous utilisons à nouveaufmt.Sprintf pour imprimer le message de contexte, puis leerror (fmt.Sprintf sait appeler implicitement la méthodeError() comme bien).

Dansmain(), nous créons une erreur en utilisanterrors.New, puis nous enveloppons cette erreur en utilisant la fonctionWrap que nous avons définie. Cela nous permet d'indiquer que ceerror a été généré en"main". De plus, puisque notreWrappedError est aussi unerror, nous pourrions envelopper d'autres `WrappedError`s - cela nous permettrait de voir une chaîne pour nous aider à retrouver la source de l'erreur. Avec un peu d'aide de la bibliothèque standard, nous pouvons même incorporer des traces de pile complètes dans nos erreurs.

Conclusion

Étant donné que l’interfaceerror n’est qu’une méthode unique, nous avons vu que nous avons une grande flexibilité pour fournir différents types d’erreurs pour différentes situations. Cela peut englober tout, de la communication de plusieurs éléments d'information dans le cadre d'une erreur à l'implémentation deexponential backoff. Bien que les mécanismes de gestion des erreurs dans Go semblent à première vue sembler simplistes, nous pouvons réaliser une gestion relativement riche en utilisant ces erreurs personnalisées pour gérer les situations courantes et peu communes.

Go a un autre mécanisme pour communiquer un comportement inattendu, la panique. Dans notre prochain article de la série de gestion des erreurs, nous examinerons les paniques - ce qu'elles sont et comment les gérer.