La gestion des erreurs en Go

Apprenez la gestion idiomatique des erreurs en Go : l’interface error, le retour et la vérification des erreurs, errors.New, l’enveloppe %w et errors.Is.

Prérequis

Objectifs pédagogiques

  • Renvoyer et vérifier des erreurs avec l’interface error et if err != nil
  • Créer des erreurs avec errors.New et ajouter du contexte en enveloppant avec %w
  • Filtrer des erreurs avec les sentinelles, errors.Is et errors.As

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 %v au lieu de %w. %v met en forme le message mais jette l’erreur d’origine : errors.Is et errors.As ne la voient plus.
  • Comparer des erreurs enveloppées avec ==. Une fois une erreur enveloppée, utilisez errors.Is pour 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é

  • error est une interface ; une erreur nil signifie 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 %w pour garder l’originale accessible.
  • Comparez les erreurs avec errors.Is (sentinelles) et errors.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.

Votre progression est enregistrée uniquement dans ce navigateur. Aucun compte n’est requis.

Recherche

La recherche s’exécute entièrement dans votre navigateur.