Cet article est une traduction de l'article d'Andrew Gerrand Error handling and Go (en anglais sur le site du projet Go).

Gestion des erreurs en Go

Si vous avez déjà écrit du code en Go, vous avez probablement été confrontés au type error. Go utilise la valeur de type error pour indiquer un état anormal. Par exemple, la fonction os.Open retourne une valeur de type error non nulle quand elle ne parvient pas à ouvrir un fichier.

func Open(name string) (file *File, err error)

La fonction suivante utilise os.Open pour ouvrir un fichier. En cas d'erreur, elle appelle log.Fatal pour afficher un message d’erreur et elle s’arrête.

f, err := os.Open("filename.ext")
if err != nil { 
	log.Fatal(err)
}
// autres actions avec le fichier ouvert - *File f

Vous pouvez faire beaucoup de choses en Go en ne connaissant seulement que error, mais dans cet article nous examinerons en détail error et nous discuterons les bonnes pratiques en Go concernant la gestion des erreurs

Le type error

Le type error est un type interface. Une variable error représente n’importe quelle valeur qui peut se décrire par une chaîne de caractères. Ci-dessous la déclaration de cette interface :

type error interface {
	Error() string
}

Le type error, comme tous les autres types intégrés, est prédéclaré dans le bloc univers.

L’implémentation du type error la plus communément utilisée est le type errorString, non exporté, du package error (NDT : en anglais sur le site de golang).

// errorString est une implémentation triviale d'error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

Vous pouvez construire une de ces valeurs avec la fonction errors.New. Elle prend une chaîne de caractères en entrée qu'elle convertit en errors.errorString puis renvoie une valeur de type error.

 // New renvoie une valeur error qui est formatée comme le texte passé en paramètre.
func New(text string) error {
    return &errorString{text}
}

Voici comment vous pourriez utiliser errors.New :

func Sqrt(f float64) (float64, error) {
	if f < 0 {
		return 0, errors.New("math: racine carrée d'un nombre négatif")
	}
	// implémentation
}

Une fonction appelante passant un argument négatif à Sqrt reçoit une valeur error non nulle (dont la représentation concrète est une valeur de type errorString). Cette fonction appelante peut accéder à une chaîne de caractères d'erreur ("math: racine carrée d'un nombre négatif") en appelant la méthode Error de la variable de type error, ou simplement en l’affichant :

f, err := Sqrt(-1) if err != nil { fmt.Println(err) }

Le paquet fmt formate une valeur de type error en appelant sa méthode Error() string.

C’est de la responsabilité de l’implémentation de error que de récupérer le contexte. L’erreur renvoyée par os.Open est est formattée ainsi : "open /etc/passwd: permission denied," et non pas simplement comme "permission denied.". Il manque néanmoins une information au sujet de l’argument invalide dans l’erreur renvoyée par notre fonction Sqrt

Pour ajouter cette information, une fonction utile est Errorf du paquet fmt. Elle formate une chaîne de caractères selon les règles de Printf et la renvoie comme une error créée par errors.new.

if f < 0 {
	return 0, fmt.Errorf("math: racine carrée d'un nombre négatif %g", f)
}

Dans de nombreux cas, la fonction fmt.Errorf est suffisante, mais comme error est une interface, vous pouvez utiliser des structures de données élaborées comme valeur d’erreur, afin de permettre aux fonctions appelantes d’inspecter en détail l’erreur renvoyée.

Par exemple, notre hypothétique fonction appelante peut vouloir récupérer l’argument invalide passé par Sqrt. Ceci est possible en définissant une nouvelle erreur à la place de errors.errorString :

type NegativeSqrtError float64 func (f NegativeSqrtError) String() string { return fmt.Sprintf(“math: racine carrée d'un nombre négatif %g”, float64(f)) }

Une fonction appelante sophistiquée peut alors utiliser une assertion de type afin de vérifier une éventuelle erreur de type NegativeSqrtError et la traiter de manière spécifique, alors que les fonctions appelantes qui passent juste l’erreur à fmt.Println ou à log.Fatal ne constateront aucun changement dans leur comportement.

Un autre exemple est le paquet json (NDT : en anglais sur le site de golang) qui spécifie un type SyntaxError que la fonction json.Decode renvoie quand elle rencontre une erreur de syntaxe lors de l'analyse d’un blob JSON.

type SyntaxError struct {
    msg    string // description de l'erreur
    Offset int64  // erreur arrivant après avoir lu les octets à Offset
}

func (e *SyntaxError) Error() string { return e.msg }

Le champ Offset ne sera même pas affiché lors du formatage par défaut de l’erreur, mais les fonctions appelantes peuvent l’utiliser pour ajouter dans un fichier des informations (la ligne concernée) à leur message d’erreur :

if err := dec.Decode(&val); err != nil {
	if serr, ok := err.(*json.SyntaxError); ok {
		line, col := findLine(f, serr.Offset)
		return fmt.Errorf("%s:%d:%d: %v", f.Name(), line, col, err)
	}
	return err
}

(Ceci est une version légèrement simplifiée du code réel du projet Camlistore) L’interface error requiert seulement une méthode Error; les implémentations spécifiques d’erreur peuvent posséder des méthodes additionnelles. Par exemple, le paquet net renvoie des erreurs de type error, suivant la convention habituelle, mais certaines implémentations d'erreur possèdent des méthodes supplémentaires définies par l’interface net.Error :

package net

type Error interface {
	error
	Timeout() bool // Est-ce que l'erreur est un timeout ?
	Temporary() bool // Est-ce que l'erreur est un temporaire ?
}

Le code client peut tester une net.Error avec une assertion de type et même distinguer les erreurs de réseau transitoires. Par exemple, il se peut qu’un crawler web s’endorme et ne se réveille que quand il rencontre une erreur temporaire et n’en à rien à faire dans les autres cas.

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    time.Sleep(1e9)
    continue
}
if err != nil {
    log.Fatal(err)
}

Simplifier la gestion des erreurs répétitives

En Go, la gestion des erreurs est quelque chose d’important. La conception du langage et les conventions adoptées vous encouragent à vérifier explicitement les erreurs à l’endroit où elle apparaissent (ce qui le distingue des conventions utilisées dans d’autres langages qui lèvent des exceptions pour les « trapper » par la suite à un niveau supérieur). Dans certains cas, ceci rend le code Go verbeux, mais heureusement il existe des techniques qui vous permettent de minimiser la gestion des erreurs répétitives.

Considérons une application App Engine avec un contrôleur HTTP qui récupère un enregistrement d’une base de données et le formate avec un template :

func init() {
    http.HandleFunc("/view", viewRecord)
}

func viewRecord(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

Cette fonction gère les erreurs renvoyées par la fonction datastore.Get et la méthode Execute de viewTemplate. Dans les deux cas, elle présente une simple erreur à l’utilisateur avec un code HTTP de statut 500 ("Internal Server Error"). Ceci semble gérable tel quel, mais ajoutez quelques contrôleurs supplémentaires et vous arriverez rapidement à dupliquer de nombreuses fois du code identique.

Pour réduire cette répétition, nous pouvons définir notre propre type appHandler HTTP qui inclut une valeur de type error :

type appHandler func(http.ResponseWriter, *http.Request) error

Nous pouvons ensuite modifier la fonction viewRecord pour renvoyer les erreurs :

func viewRecord(w http.ResponseWriter, r *http.Request) error {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return err
    }
    return viewTemplate.Execute(w, record)
}

C’est plus simple que la version originale, mais le paquet http ne comprend pas les fonctions qui retournent error.Pour corriger ceci, nous pouvons implémenter la méthode ServeHTTP de l’interface http.Handler sur appHandler :

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
} 

La méthode ServeHTTP appelle la fonction appHandler et affiche l’erreur renvoyée (si présente) à l’utilisateur. Notez bien que le récepteur de la méthode est une fonction (Go peut le faire !) ; la méthode invoque la fonction en appelant le récepteur dans l’expression fn(w, r).

Maintenant lors de l’enregistrement de viewRecord avec le paquet http, nous utilisons la fonction Handle (à la place de HandleFunc) puisque appHandler est un http.Handler (et pas un http.HandlerFunc).

func init() {
	http.Handle("/view", appHandler(viewRecord))
}

Nous pouvons rendre plus convivial ce système basique de gestion des erreurs. Plutôt que d'afficher seulement le message d’erreur, il serait souhaitable de renvoyer à l’utilisateur un simple message d’erreur avec le code de statut HTTP approprié, tandis qu'on tracera le code d’erreur complet sur la console développeur de l'App Engine dans un but de débogage.

Pour ce faire, créons une structure appError contenant un champ de type error et quelques autres champs :

type appError struct {
    Error   error
    Message string
    Code    int
}

Ensuite, nous modifions le type appHandler pour renvoyer des valeurs de type *appError :

type appHandler func(http.ResponseWriter, *http.Request) *appError

(Ce n’est habituellement pas judicieux de passer en retour le type concret d’une erreur plutôt qu’une error, pour des raisons qui seront discutées dans un article futur, mais c’est une bonne chose de le faire ici parce que ServeHTTP est le seul endroit où l’on peut voir les valeurs et utiliser leur contenu).

Et faire en sorte que la méthode ServeHTTP de appHandler affiche le champ Message de appError à l’utilisateur avec le Code HTTP de statut correct et trace l’Error complète sur la console développeur :

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if e := fn(w, r); e != nil { // e est de type *appError et pas error
        c := appengine.NewContext(r)
        c.Errorf("%v", e.Error)
        http.Error(w, e.Message, e.Code)
    }
}

Enfin, nous mettons à jour viewRecord avec la nouvelle signature de fonction et nous pouvons ainsi renvoyer plus de contexte quand une erreur survient dans l’application :

func viewRecord(w http.ResponseWriter, r *http.Request) *appError {
    c := appengine.NewContext(r)
    key := datastore.NewKey(c, "Record", r.FormValue("id"), 0, nil)
    record := new(Record)
    if err := datastore.Get(c, key, record); err != nil {
        return &appError{err, "Record not found", 404}
    }
    if err := viewTemplate.Execute(w, record); err != nil {
        return &appError{err, "Can't display record", 500}
    }
    return nil
}

Cette version de viewRecord fait la même longueur que l’originale, mais maintenant chacune de ces lignes possède une signification spécifique et nous pouvons fournir à l’utilisateur des informations d’erreur plus conviviales.

Nous pouvons encore améliorer cette gestion des erreurs avec ces quelques idées :

  • Fournir au gestionnaire d’erreurs un template HTML pour le formatage de l’erreur,
  • Rendre le debuggage plus aisé en écrivant la trace de la pile dans la réponse HTTP quand l’utilisateur est un administrateur,
  • Écrire un constructeur pour appError qui sauvegarde la trace de la pile pour améliorer le débogage,
  • Récupérer des paniques d'exécution (panic) à l’intérieur de appHandler, tracer l’erreur sur la console comme "Critical," pendant que l’on informe l’utilisateur qu’une "erreur grave est survenue". C’est une attention délicate d’éviter d’exposer l’utilisateur à des messages d’erreur impénétrables causés par des erreurs de programmation. Voir l’article Defer, Panic, and Recover pour plus de détail.

Conclusion

Une gestion appropriée des erreurs est une exigence essentielle d'un bon logiciel. En employant les techniques décrites dans cet article, vous devriez pouvoir écrire du code plus fiable et succinct


Étiquettes :   golang   debuter 
Portrait de Benjamin BALET
Benjamin BALET
Consultant APM

Retrouvez mes cooordonées

Benjamin BALET sur viadeo






Vous aimerez aussi

Les lois de la réflexion

Une traduction du blog officiel de golang expliquant le mécanisme de la réflexion en Go.   Lire »

Gobs le format natif d'échange de données en Go

Traduction d'un article du blog officiel expliquant comment échanger des données entre deux programmes golang grâce à un format natif   Lire »

Comment écrire du code Go ?

Traduction d'une partie des spécifications officielles du langage Go, cet article explique comment développer en Go.   Lire »

Canaux et go routines avec ou sans état

Exemples d'utilisation du type chan et des go routines stateful et stateless   Lire »

Le modèle mémoire du runtime et du langage go

Traduction d'une partie des spécifications officielles du langage go, cet article explique comment go gère la mémoire.   Lire »

Commentaires

Soyez le premier à commenter cet article

Publier un commentaires

Tous les commentaires sont soumis à modération. Les balises HTML sont pour la plupart autorisées. Les liens contextuels et intéressants sont en follow.

(requis)
(requis)
(requis, mais non publié)
(recommandé si vous souhaitez être publié)