Contrairement à des langages tels que Python, Java ou JavaScript, Go ne dispose pas de blocs try/catch ni de mécanisme d'exception intégré. Au lieu de cela, Go adopte la philosophie selon laquelle les erreurs sont des valeurs — des citoyens de première classe qui doivent être gérés explicitement au point d'apparition. Pour les développeurs de niveau intermédiaire à avancé, comprendre comment gérer ces erreurs efficacement est crucial pour construire des applications robustes, maintenables et faciles à déboguer. Cet article explore l'évolution de la gestion des erreurs en Go, passant des patterns basiques aux techniques avancées telles que l'incorporation d'erreurs (error wrapping) et les types d'erreurs personnalisés.
L'anatomie d'une erreur Go
Au cœur du système, le type error en Go est simplement une interface avec une seule méthode :
type error interface {
Error() string
}
Cette simplicité est à la fois la force et la complexité de Go. Puisque tout type implémentant cette interface peut être renvoyé en tant qu'erreur, les développeurs disposent d'une grande flexibilité. Cependant, cela signifie également que la gestion des erreurs nécessite souvent des assertions de type, de l'incorporation (wrapping) et une réflexion attentive sur les types d'erreurs avant de décider d'une stratégie de récupération.
Gestion basique des erreurs et erreurs sentinelles
Le pattern le plus courant consiste à vérifier la présence d'une erreur non nulle immédiatement après un appel de fonction. Bien que simple, la comparaison directe des erreurs à l'aide de errors.Is() est essentielle. Dans les anciennes versions de Go, les développeurs utilisaient des « erreurs sentinelles » — des variables d'erreur globales prédéfinies — pour représenter des conditions d'échec spécifiques.
var ErrNotFound = errors.New("record not found")
func FindUser(id int) (*User, error) {
if id == 0 {
return nil, ErrNotFound
}
// ... logique pour trouver l'utilisateur
}
Pour vérifier si une erreur correspond à une sentinelle, utilisez toujours errors.Is() plutôt qu'une égalité directe (==). Cela garantit la compatibilité avec l'incorporation d'erreurs, un sujet que nous aborderons ensuite.
Envelopper les erreurs pour ajouter du contexte
À mesure que les applications grandissent, les messages d'erreur simples perdent souvent leur contexte. Lorsqu'un appel à la base de données échoue au sein d'une couche de service, savoir que le package sql a renvoyé une erreur est moins utile que de savoir que la fonction GetUser a échoué lors de la tentative de récupération de l'ID utilisateur 123.
Go 1.13 a introduit le verbe %w dans le package fmt, permettant aux développeurs d'envelopper (wrap) les erreurs. Cela préserve la pile d'erreurs originale tout en ajoutant une couche de contexte.
func GetUser(id int) (*User, error) {
user, err := db.GetUserByID(id)
if err != nil {
// Envelopper l'erreur avec un contexte supplémentaire
return nil, fmt.Errorf("fetching user %d: %w", id, err)
}
return user, nil
}
Lors de l'enveloppement d'erreurs, errors.Is() et errors.As() traverseront la chaîne d'erreurs enveloppées. Cela signifie que vous pouvez vérifier la présence d'un type d'erreur sous-jacent spécifique, même s'il a été enveloppé plusieurs fois.
Types d'erreurs personnalisés et errors.As()
Parfois, vous devez gérer les erreurs différemment en fonction de leur type. Par exemple, vous pourriez vouloir renvoyer un code d'état 404 pour une erreur « non trouvé » et un code d'état 500 pour un « délai d'attente de connexion à la base de données ». Pour cela, vous avez besoin de types d'erreurs personnalisés et de la fonction errors.As().
type TimeoutError struct {
Operation string
Duration time.Duration
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("%s operation timed out after %v", e.Operation, e.Duration)
}
func HandleRequest(w http.ResponseWriter, err error) {
var timeoutErr *TimeoutError
if errors.As(err, &timeoutErr) {
http.Error(w, "Service unavailable", http.StatusServiceUnavailable)
return
}
// Gérer les autres erreurs
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
errors.As() tente de trouver la première erreur de la chaîne qui correspond au type cible. C'est plus sûr que les assertions de type car cela fonctionne de manière transparente avec les erreurs enveloppées.
Meilleures pratiques pour le code de production
- Ne pas ignorer les erreurs : Même si vous vous attendez à une erreur, gérez-la ou journalisez-la. Ignorer les erreurs conduit à des échecs silencieux.
- Envelopper au bon niveau : Enveloppez les erreurs lorsque vous ajoutez un contexte significatif. N'enveloppez pas les erreurs qui sont immédiatement renvoyées à l'utilisateur sans modification.
- Utiliser
errors.Ispour les vérifications : Préférez toujourserrors.Is()à la comparaison directe. - Gardez les messages d'erreur concis : Lorsque vous renvoyez des erreurs aux utilisateurs, supprimez les détails techniques qui pourraient divulguer des informations sur l'implémentation interne.
Conclusion
La gestion des erreurs en Go ne consiste pas à éviter la complexité, mais à la rendre explicite. En maîtrisant les erreurs sentinelles, l'enveloppement d'erreurs avec fmt.Errorf et la vérification de type avec errors.As(), vous pouvez écrire des applications Go qui sont non seulement fonctionnelles, mais aussi résilientes et faciles à déboguer. Adoptez la verbosité de la gestion des erreurs de Go ; elle paie des dividendes à long terme pour la maintenabilité de votre base de code.