Gestion des Erreurs en Go : Patterns, Wrapping et Bonnes Pratiques en 2026

Guide complet sur la gestion des erreurs en Go : types personnalisés, sentinel errors, error wrapping avec errors.Is et errors.As, anti-patterns à éviter et traitement concurrent avec errgroup.

Illustration des patterns de gestion des erreurs en Go avec error wrapping et bonnes pratiques

La gestion des erreurs constitue l'un des aspects les plus distinctifs du langage Go. Contrairement aux exceptions utilisées dans de nombreux langages, Go adopte une approche explicite où chaque erreur potentielle doit être traitée au moment où elle survient. Cette philosophie, parfois critiquée pour sa verbosité, garantit un code robuste et prévisible lorsqu'elle est correctement appliquée.

En 2026, les patterns de gestion d'erreurs en Go ont considérablement mûri. L'introduction de errors.Is et errors.As dans Go 1.13, combinée aux améliorations successives du runtime, offre désormais un arsenal complet pour gérer les erreurs de manière élégante et maintenable. Cet article explore les patterns essentiels que tout développeur Go doit maîtriser.

Préparation aux entretiens techniques

La gestion des erreurs représente un sujet fréquemment abordé lors des entretiens Go. Les recruteurs évaluent non seulement la connaissance des APIs standards, mais aussi la capacité à concevoir des stratégies d'erreurs cohérentes à l'échelle d'une application.

L'interface error : fondement du système

L'interface error en Go se distingue par sa simplicité remarquable. Elle ne définit qu'une seule méthode :

builtin.gogo
type error interface {
    Error() string
}

Cette simplicité constitue à la fois une force et une responsabilité. N'importe quel type implémentant la méthode Error() string satisfait l'interface. Cette flexibilité permet de créer des types d'erreurs riches en informations contextuelles, tout en maintenant une compatibilité universelle avec l'écosystème Go.

Le pattern idiomatique de retour d'erreur place systématiquement l'erreur en dernière position des valeurs de retour. Cette convention, profondément ancrée dans la culture Go, facilite la lecture du code et permet d'identifier immédiatement les fonctions susceptibles d'échouer.

Types d'erreurs personnalisés

Les types d'erreurs personnalisés permettent d'encapsuler des informations structurées sur la nature de l'erreur. Ils s'avèrent particulièrement utiles lorsque le contexte de l'erreur influence la logique de traitement :

apperror.gogo
type NotFoundError struct {
    Resource string
    ID       string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}

Ce pattern offre plusieurs avantages significatifs. Le code appelant peut extraire les informations structurées (Resource, ID) pour personnaliser sa réponse. Les tests unitaires peuvent vérifier précisément le type et le contenu des erreurs. La documentation du code s'enrichit naturellement des champs exposés par le type.

La convention recommande d'utiliser des pointeurs de structure comme receveurs pour les méthodes Error(). Cette approche évite les allocations inutiles et permet la comparaison par identité lorsque nécessaire.

Sentinel errors : constantes d'erreur

Les sentinel errors représentent des valeurs d'erreur prédéfinies au niveau du package. Elles servent de points de référence pour identifier des conditions d'erreur spécifiques :

errors.gogo
var (
    ErrNotFound     = errors.New("record not found")
    ErrUnauthorized = errors.New("unauthorized access")
    ErrConflict     = errors.New("resource conflict")
)

La convention de nommage Err suivie d'un nom descriptif facilite l'identification de ces valeurs dans le code. Les sentinel errors conviennent particulièrement aux cas où l'existence même de l'erreur suffit, sans nécessiter d'informations contextuelles supplémentaires.

L'utilisation judicieuse des sentinel errors améliore la lisibilité du code en établissant un vocabulaire commun au sein d'un package ou d'une application. Les consommateurs du package peuvent ainsi s'appuyer sur ces valeurs exportées pour structurer leur logique de gestion d'erreurs.

Error wrapping : préserver le contexte

L'error wrapping permet d'enrichir une erreur existante avec du contexte additionnel tout en préservant l'erreur originale. Le verbe %w dans fmt.Errorf crée cette relation de wrapping :

repository.gogo
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
    user, err := r.db.QueryContext(ctx, "SELECT * FROM users WHERE id = $1", id)
    if err != nil {
        return nil, fmt.Errorf("UserRepo.FindByID(%s): %w", id, err)
    }
    return user, nil
}

Ce pattern construit une chaîne d'erreurs qui documente le chemin d'exécution ayant mené à l'échec. Chaque couche applicative ajoute son contexte spécifique, créant ainsi une trace exploitable pour le débogage.

La clé réside dans l'équilibre entre information et bruit. Le contexte ajouté doit apporter une valeur diagnostique sans dupliquer les informations déjà présentes dans l'erreur sous-jacente. L'inclusion du nom de la fonction et des paramètres clés constitue généralement une bonne pratique.

Prêt à réussir tes entretiens Go ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

errors.Is : vérification d'identité d'erreur

La fonction errors.Is parcourt la chaîne d'erreurs wrappées pour déterminer si une erreur spécifique s'y trouve. Elle remplace avantageusement les comparaisons directes qui échouaient avec les erreurs wrappées :

handler.gogo
func handleGetUser(w http.ResponseWriter, r *http.Request) {
    user, err := userService.GetByID(r.Context(), chi.URLParam(r, "id"))
    if errors.Is(err, ErrNotFound) {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, "Internal error", http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(user)
}

L'ordre des vérifications importe. Les erreurs les plus spécifiques doivent être testées en premier, suivies des cas généraux. La vérification err != nil en dernier attrape toutes les erreurs non identifiées.

La fonction errors.Is fonctionne correctement avec les sentinel errors et tout type implémentant la méthode Is(error) bool pour une logique de comparaison personnalisée.

errors.As : extraction de type d'erreur

Lorsque le traitement nécessite d'accéder aux champs d'un type d'erreur spécifique, errors.As permet d'extraire et de convertir l'erreur vers ce type :

middleware.gogo
func errorMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                http.Error(w, "Internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func mapErrorToHTTP(err error) int {
    var notFound *NotFoundError
    if errors.As(err, &notFound) {
        return http.StatusNotFound
    }
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        return http.StatusBadRequest
    }
    return http.StatusInternalServerError
}

La variable cible doit être un pointeur vers le type recherché. En cas de succès, errors.As assigne l'erreur trouvée à cette variable, permettant ensuite d'accéder à ses champs et méthodes spécifiques.

Ce mécanisme s'avère particulièrement puissant pour centraliser la logique de mapping entre erreurs domaine et réponses HTTP dans un middleware ou une fonction utilitaire.

Erreurs domaine structurées

Les applications d'envergure bénéficient d'un type d'erreur domaine standardisé. Ce type encapsule un code d'erreur, un message lisible et optionnellement l'erreur sous-jacente :

domain/errors.gogo
type DomainError struct {
    Code    string
    Message string
    Err     error
}

func (e *DomainError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("%s: %s: %v", e.Code, e.Message, e.Err)
    }
    return fmt.Sprintf("%s: %s", e.Code, e.Message)
}

func (e *DomainError) Unwrap() error {
    return e.Err
}

La méthode Unwrap permet à errors.Is et errors.As de traverser la chaîne d'erreurs correctement. Sans cette méthode, l'erreur encapsulée dans le champ Err resterait inaccessible aux fonctions d'inspection.

Les codes d'erreur (ALREADY_INACTIVE, VALIDATION_FAILED, etc.) facilitent l'internationalisation et le logging structuré. Ils permettent également aux clients API de réagir programmatiquement aux différentes conditions d'erreur.

Couche service et propagation d'erreurs

La couche service orchestre la logique métier et constitue un point stratégique pour la création et la propagation d'erreurs :

service/user.gogo
func (s *UserService) Deactivate(ctx context.Context, userID string) error {
    user, err := s.repo.FindByID(ctx, userID)
    if err != nil {
        return fmt.Errorf("deactivating user %s: %w", userID, err)
    }
    if user.Status == StatusInactive {
        return &DomainError{
            Code:    "ALREADY_INACTIVE",
            Message: fmt.Sprintf("user %s is already inactive", userID),
        }
    }
    return s.repo.UpdateStatus(ctx, userID, StatusInactive)
}

Cette approche distingue clairement les erreurs techniques (provenant du repository) des erreurs métier (violation de règles domaine). Les premières sont wrappées pour préserver le contexte, tandis que les secondes sont créées explicitement avec les informations pertinentes.

Questions fréquentes en entretien technique

Les entretiens techniques Go abordent régulièrement ces aspects de la gestion d'erreurs :

Quelle différence entre errors.Is et errors.As ? errors.Is vérifie si une erreur spécifique existe dans la chaîne (comparaison d'identité), tandis que errors.As tente d'extraire une erreur d'un type donné (conversion de type). Le premier répond à "cette erreur est-elle présente ?", le second à "puis-je accéder aux détails de ce type d'erreur ?".

Quand utiliser un type personnalisé versus un sentinel error ? Les types personnalisés conviennent lorsque l'erreur transporte des données structurées nécessaires au traitement. Les sentinel errors suffisent quand seule l'existence de l'erreur importe, sans contexte additionnel.

Comment gérer les erreurs dans les goroutines ? Les channels ou le package errgroup permettent de collecter et propager les erreurs des goroutines vers la goroutine principale. L'errgroup offre une API plus ergonomique avec annulation automatique du contexte.

Anti-patterns à éviter

Certaines pratiques nuisent à la maintenabilité et à la fiabilité du code. L'ignoration silencieuse des erreurs constitue le piège le plus courant :

go
// anti-pattern: silent error swallowing
result, _ := riskyOperation()

Cette pratique masque des échecs potentiellement critiques. Si l'erreur ne peut être traitée localement, elle doit être propagée au niveau supérieur.

Le double traitement représente un autre anti-pattern fréquent :

go
// anti-pattern: double handling
if err != nil {
    log.Printf("operation failed: %v", err)
    return err
}

Logger puis retourner l'erreur provoque des logs dupliqués lorsque chaque niveau de la pile d'appels adopte cette approche. La règle recommande de traiter l'erreur (logger, convertir, etc.) ou de la propager, mais pas les deux simultanément.

Traitement concurrent avec errgroup

Le package golang.org/x/sync/errgroup simplifie la gestion des erreurs dans les traitements concurrents. Il coordonne plusieurs goroutines et collecte leurs erreurs :

batch.gogo
func (s *OrderService) ProcessBatch(ctx context.Context, orderIDs []string) error {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(10)

    for _, id := range orderIDs {
        g.Go(func() error {
            if err := s.processOrder(ctx, id); err != nil {
                return fmt.Errorf("processing order %s: %w", id, err)
            }
            return nil
        })
    }

    return g.Wait()
}

L'errgroup annule automatiquement le contexte dès qu'une goroutine retourne une erreur, permettant aux autres goroutines de s'interrompre proprement. La méthode SetLimit contrôle le parallélisme pour éviter la saturation des ressources.

La méthode Wait bloque jusqu'à la terminaison de toutes les goroutines et retourne la première erreur rencontrée. Pour collecter toutes les erreurs, des patterns alternatifs utilisant des channels ou le type errors.Join de Go 1.20+ s'avèrent nécessaires.

Nouveautés Go 1.24

Go 1.24 introduit des améliorations au niveau du runtime pour le traitement des erreurs, notamment une meilleure intégration avec les outils de tracing distribué. Les stack traces attachées aux erreurs wrappées bénéficient également d'optimisations de performance significatives.

Conclusion

La gestion des erreurs en Go repose sur des principes simples mais exigeants. Une application robuste nécessite une stratégie cohérente appliquée à travers toutes les couches du code.

Les points essentiels à retenir :

  • L'interface error offre une flexibilité maximale grâce à sa simplicité
  • Les types personnalisés encapsulent le contexte structuré nécessaire au traitement
  • Les sentinel errors établissent un vocabulaire commun pour les conditions d'erreur standard
  • L'error wrapping avec %w préserve la chaîne causale pour le débogage
  • errors.Is et errors.As permettent l'inspection robuste des erreurs wrappées
  • L'errgroup simplifie la collecte d'erreurs dans les traitements concurrents
  • Éviter l'ignoration silencieuse et le double traitement des erreurs

La maîtrise de ces patterns distingue le code Go professionnel du code amateur. En entretien comme en production, une gestion d'erreurs rigoureuse témoigne d'une compréhension approfondie du langage et de ses idiomes.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#go
#error-handling
#best-practices
#interview
#golang

Partager

Articles similaires