Go n’a pas d’exceptions. Une fonction susceptible d’échouer renvoie une error
comme dernier résultat, et l’appelant la vérifie. Le chemin d’échec devient
ainsi explicite et visible là où il se produit.
L’interface error
error n’est qu’une interface intégrée dotée d’une seule méthode. Tout type
possédant une méthode Error() string est une erreur.
type error interface {
Error() string
}
Une erreur nil signale un succès ; une erreur non nil décrit ce qui a mal
tourné.
Renvoyer et vérifier des erreurs
Le motif idiomatique consiste à renvoyer l’erreur en dernier et à la vérifier
immédiatement avec if err != nil. Vous la traitez, ou vous la renvoyez à votre
propre appelant.
f, err := os.Open("config.txt")
if err != nil {
return err // traitez-la ici, ou propagez-la
}
defer f.Close()
Créer des erreurs avec errors.New
Pour un message simple et fixe, utilisez errors.New. Pour un message qui
inclut des valeurs, utilisez fmt.Errorf.
err := errors.New("division par zéro impossible")
err = fmt.Errorf("l'utilisateur %d n'est pas autorisé", id)
Envelopper les erreurs avec %w
Lorsque vous propagez une erreur, ajoutez du contexte pour que le message final
se lise comme une piste. Le verbe %w de fmt.Errorf enveloppe l’erreur
d’origine, qui reste accessible pour une inspection ultérieure.
if err != nil {
return fmt.Errorf("chargement de la config : %w", err)
}
Erreurs sentinelles et errors.Is
Une sentinelle est une valeur d’erreur prédéfinie que les appelants comparent.
Comme %w préserve l’erreur enveloppée, utilisez errors.Is (et non ==) pour
que la comparaison fonctionne à travers les couches d’enveloppement.
var ErrNotFound = errors.New("introuvable")
if errors.Is(err, ErrNotFound) {
// traiter le cas « introuvable »
}
Types d’erreur personnalisés et errors.As
Lorsque vous devez attacher des données à une erreur, définissez un type doté
d’une méthode Error. errors.As déballe la chaîne et, s’il trouve un type
correspondant, remplit votre variable pour que vous puissiez lire ses champs.
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return "champ invalide : " + e.Field
}
var vErr *ValidationError
if errors.As(err, &vErr) {
fmt.Println("champ fautif :", vErr.Field)
}
Un exemple complet
Le programme ci-dessous recherche un utilisateur, enveloppe une erreur
sentinelle avec du contexte, et utilise errors.Is pour réagir au cas
« introuvable ». Il compile et s’exécute tel quel.
package main
import (
"errors"
"fmt"
)
// ErrNotFound est une erreur sentinelle que les appelants peuvent comparer.
var ErrNotFound = errors.New("user not found")
var users = map[int]string{
1: "Ada",
2: "Alan",
}
// findUser renvoie le nom, ou enveloppe ErrNotFound avec du contexte.
func findUser(id int) (string, error) {
name, ok := users[id]
if !ok {
return "", fmt.Errorf("findUser %d: %w", id, ErrNotFound)
}
return name, nil
}
func main() {
for _, id := range []int{1, 42} {
name, err := findUser(id)
if err != nil {
if errors.Is(err, ErrNotFound) {
fmt.Printf("id %d: not found\n", id)
continue
}
fmt.Printf("id %d: unexpected error: %v\n", id, err)
continue
}
fmt.Printf("id %d: %s\n", id, name)
}
}
Son exécution affiche :
id 1: Ada
id 42: not found
Nettoyage avec defer, et panic en dernier recours
defer planifie un appel qui s’exécutera au retour de la fonction englobante,
quel que soit le chemin emprunté. C’est la façon standard de libérer des
ressources comme des fichiers ou des verrous, en plaçant le nettoyage juste à
côté de l’acquisition.
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close() // s'exécute toujours à la sortie
Erreurs fréquentes
- Ignorer les erreurs. Écrire
f, _ := os.Open(...)masque les échecs. Traitez ou renvoyez chaque erreur reçue. - Envelopper avec
%vau lieu de%w.%vmet en forme le message mais jette l’erreur d’origine :errors.Iseterrors.Asne la voient plus. - Comparer des erreurs enveloppées avec
==. Une fois une erreur enveloppée, utilisezerrors.Ispour comparer une sentinelle. - Paniquer sur des échecs ordinaires. Un fichier manquant ou une entrée invalide sont attendus ; renvoyez une erreur plutôt que de faire planter le programme.
À vous de jouer
Écrivez une fonction divide(a, b int) (int, error) qui renvoie une erreur créée
avec errors.New lorsque b vaut zéro, puis appelez-la avec un diviseur nul et
affichez l’erreur.
Afficher la correction
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("result:", result)
}divide renvoie l’erreur en dernière valeur ; main la vérifie avec
if err != nil avant de toucher à result. Avec un diviseur nul, la garde se
déclenche et le programme affiche error: cannot divide by zero.
Résumé
errorest une interface ; une erreurnilsignifie un succès.- Renvoyez les erreurs en dernier et vérifiez-les avec
if err != nil. - Créez des erreurs avec
errors.New/fmt.Errorf, et enveloppez avec%wpour garder l’originale accessible. - Comparez les erreurs avec
errors.Is(sentinelles) eterrors.As(types personnalisés).
Ensuite, revenez sur les interfaces en Go sur
lesquelles error lui-même repose. Pour voir où les erreurs apparaissent
d’abord comme valeurs de retour, relisez les
fonctions en Go.