Des exceptions en Go

Publié le 2025-12-05 par DarkChyper

Dans la philosophie du langage Golang, la gestion des erreurs doit se faire au plus proche de la source. Que ce soit dans son propre code ou via des dépendances, il est très courant, lors de l’appel d’une fonction, de récupérer un résultat et une erreur. Erreur qui vaut, dans la majorité des cas, nil.

 GO

package main import ( "errors" "fmt" "strings" ) func loadFile(name string) (string, error) { if strings.Contains(name, "missing") { return "", errors.New("fichier introuvable") } return "DATA:" + name, nil } func parseData(data string) (string, error) { if strings.Contains(data, "bad") { return "", errors.New("données invalides") } return strings.ToUpper(data), nil } func storeResult(name, data string) error { if strings.Contains(name, "readonly") { return errors.New("impossible d'écrire le résultat (readonly)") } fmt.Println("Résultat stocké pour", name, ":", data) return nil } func main() { files := []string{ "file1", // OK "file_missing", // loadFile => erreur "file_bad_format", // parseData => erreur "file_readonly", // storeResult => erreur "file2", // OK } for _, f := range files { fmt.Println("== Traitement de", f) data, err := loadFile(f) if err != nil { fmt.Println("Erreur loadFile:", err) continue } parsed, err := parseData(data) if err != nil { fmt.Println("Erreur parseData:", err) continue } if err := storeResult(f, parsed); err != nil { fmt.Println("Erreur storeResult:", err) continue } fmt.Println("Traitement terminé avec succès pour", f) } fmt.Println("\n=== Fin du programme ===") }

Cette gestion permet au programme Go de continuer son exécution et d’éviter les crashs violents.

Mais parfois, on écrit du code dont l’objectif est justement de faire échouer l’exécution dès la première erreur sérieuse.

Typiquement : un script d’installation, un outil système, un setup de machine… bref, des moments où si quelque chose ne va pas, on veut que ça s’arrête tout de suite.

 GO

retour, err := MaFonction() if err != nil { fmt.Println("ERROR:", err) os.Exit(-1) } retour2, err := MaFonction2() if err != nil { fmt.Println("ERROR:", err) os.Exit(-1) }

À la longue, ce style devient verbeux, répétitif et franchement pas agréable à lire.

C’est là qu’on peut choisir une approche différente : factoriser la gestion d’erreur dans une fonction utilitaire qui se charge d’interrompre le programme dès qu’une erreur survient.

 GO

func check(e error) { if e != nil { panic(fmt.Sprintf("Erreur fatale : %v", e)) } }

L’idée est simple : à chaque fois qu’une fonction retourne une erreur que l’on considère bloquante, on appelle check(err).

S’il n’y a rien à signaler, le programme continue.

Sinon on déclenche un panic volontairement.

Cela dit, cette manière de faire montre rapidement ses limites. Si une fonction interne appelle elle-même check(), elle pourra déclencher un panic que l’appelant n’attendait pas forcément. On se retrouve alors avec un mélange de fonctions "sûres" et d'autres "qui paniquent" dans une même base de code.

On s'éloigne beaucoup trop de la philosophie du langage, qui nous donne pourtant un trio d'outils très puissant :

En les combinant, on peut factoriser la gestion d'erreur tout en gardant un code propre.

Attention, cette approche n’est pas compatible avec tous les types de programmes.
Une API, un serveur, ou n’importe quel service censé tourner longtemps sans faire un caprice, ne devraient surtout pas utiliser cette méthode. Dans ces cas-là, on reste sur la gestion d’erreurs explicite de Go. C’est un peu plus verbeux, mais nettement plus sain.
Disons qu’il vaut mieux faire tomber une seule goroutine que de kill tout Netflix parce qu’un développeur PHP a décidé d’essayer Go :troll:
 GO

func loadConfig(path string) string { fmt.Println("loadConfig:", path) if _, err := os.Stat(path); err != nil { panic(fmt.Errorf("impossible de charger le fichier %s : %w", path, err)) } return "mode=fast;version=1.2" }

Lorsqu’un panic est lancé, il remonte toute la pile d’exécution et termine le programme.

Pour l’intercepter proprement là où on en a besoin, on utilise recover :

 GO

err := recover() if err != nil { fmt.Println("ERROR:", err) os.Exit(-1) }

Et pour que ce recover s’exécute automatiquement à la fin d’un bloc (par exemple au niveau de main) on le place dans un defer. Ce defer peut être global ou appliqué seulement à une section critique du programme.

 GO

defer func() { if r := recover(); r != nil { fmt.Println("Le programme a rencontré une erreur : ", r) os.Exit(-1) } }()

On obtient ainsi l’équivalent d’un throw => catch => finally que l’on retrouve dans d’autres langages.

Voici maintenant un exemple complet pour visualiser clairement le flux :

 GO

package main import ( "fmt" "os" "strings" ) func main() { // Defer global : capture tout panic provenant du programme defer func() { if r := recover(); r != nil { fmt.Println("Le programme a rencontré une erreur : ", r) os.Exit(-1) } }() fmt.Println("=== Début du programme ===") cfg := loadConfig("config.txt") value := readValue(cfg, "mode") result := computeResult(value) fmt.Println("Résultat final:", result) fmt.Println("=== Fin normale du programme ===") } func loadConfig(path string) string { fmt.Println("loadConfig:", path) if _, err := os.Stat(path); err != nil { panic(fmt.Errorf("impossible de charger le fichier %s : %w", path, err)) } // on simule un contenu return "mode=fast;version=1.2" } func readValue(cfg, key string) string { fmt.Println("readValue:", key) parts := strings.Split(cfg, ";") for _, p := range parts { if strings.HasPrefix(p, key+"=") { return strings.TrimPrefix(p, key+"=") } } panic(fmt.Errorf("clé '%s' introuvable dans la config", key)) } func computeResult(mode string) string { fmt.Println("computeResult:", mode) if mode == "fast" { return "OK-FAST" } if mode == "slow" { return "OK-SLOW" } panic(fmt.Errorf("mode '%s' inconnu", mode)) }

Ici le defer est au tout début de la pile, donc dès qu'il est déclenché, le programme se termine totalement. Si on place ce defer au début d'une fonctionnalité critique, seule cette dernière tombera proprement en erreur et pas tout le programme.

Merci à mon collègue Pierre de m'avoir montré cette approche.