Gestion des erreurs en Go : philosophie sans try/catch
Dans le monde de Go (Golang), il n'existe pas de mécanisme try/catch familier pour de nombreux développeurs. Ce n'est pas un hasard, mais une décision architecturale délibérée. Les créateurs du langage ont estimé que la gestion explicite des erreurs rend le code plus fiable et plus lisible. Au lieu de cacher les exceptions dans la pile d'appels, Go propose au programmeur de toujours vérifier le résultat de chaque opération potentiellement dangereuse. Dans cet article, nous allons passer en revue toutes les approches principales de la gestion des erreurs en Go : des error de base aux puissants panic et recover, et montrer comment écrire un code qui ne plantera pas au moment le plus inopportun.
1. Mécanisme de base : l'interface error
La base de la gestion des erreurs en Go est l'interface intégrée error. Elle n'a qu'une seule méthode :
type error interface { Error() string}Toute fonction qui peut échouer doit retourner error comme dernière valeur. Il est convenu de retourner nil s'il n'y a pas d'erreur, et une valeur non nulle en cas d'échec.
Exemple de gestion de base :
package main
import ( "errors" "fmt")
func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("division par zéro impossible") } return a / b, nil}
func main() { result, err := divide(10, 0) if err != nil { fmt.Println("Erreur :", err) return } fmt.Println("Résultat :", result)}C'est l'équivalent du try/catch en Go — une vérification explicite if err != nil. Cette approche oblige le développeur à penser aux erreurs à chaque étape.
2. Types d'erreurs personnalisés et wrapping
Souvent, il ne suffit pas de retourner une simple chaîne de description. Il faut transmettre le contexte : où l'erreur s'est produite, quels paramètres ont été passés. Pour cela, on crée ses propres types d'erreurs.
Création d'une erreur personnalisée :
type DivisionError struct { Dividend float64 Divisor float64 Message string}
func (e *DivisionError) Error() string { return fmt.Sprintf("erreur de division : %s (dividende : %f, diviseur : %f)", e.Message, e.Dividend, e.Divisor)}
func safeDivide(a, b float64) (float64, error) { if b == 0 { return 0, &DivisionError{Dividend: a, Divisor: b, Message: "le diviseur est nul"} } return a / b, nil}Wrapping des erreurs :
Depuis Go 1.13, il est possible d'encapsuler une erreur dans une autre, en ajoutant du contexte :
import "fmt"
func readConfig(path string) error { file, err := openFile(path) if err != nil { return fmt.Errorf("impossible de lire le fichier de configuration %s : %w", path, err) } defer file.Close() // ... lecture du fichier return nil}La directive %w crée une chaîne d'erreurs. Plus tard, vous pouvez vérifier si une erreur est d'un type spécifique avec errors.Is() et errors.As().
3. Panic et Recover : situations d'urgence
Parfois, il se produit quelque chose qui ne devrait jamais arriver — dépassement des limites d'un tableau, déréférencement d'un pointeur nul. Dans ces cas, Go utilise panic. Ce n'est pas un remplacement du try/catch, mais un mécanisme pour les erreurs critiques et irrécupérables.
Exemple de panic :
func main() { arr := []int{1, 2, 3} fmt.Println(arr[10]) // panic: runtime error: index out of range}Cependant, si vous voulez "attraper" une panique et reprendre l'exécution, on utilise recover en combinaison avec defer. C'est l'équivalent le plus proche du catch.
Exemple de recover :
func safeCall(f func()) { defer func() { if r := recover(); r != nil { fmt.Pr