backend / go
Defer, panic et recover
Explication
Ce que vous allez apprendre
- Comprendre l'ordre d'exécution LIFO (pile) de plusieurs
defer - Utiliser
deferpour garantir un nettoyage, même en cas de sortie anticipée - Distinguer
panic(exceptionnel) deerror(flux normal d'échec) - Récupérer une panique avec
recover()et transformer un crash en erreur gérée - Isoler une panique par requête dans un serveur pour ne pas faire tomber tout le service
Dans quel contexte ?
Un serveur HTTP traite des milliers de requêtes par seconde, et l'une d'entre elles déclenche un accès hors limites sur un slice à cause d'une donnée client mal formée. Sans mécanisme de récupération, cette seule requête ferait planter tout le processus serveur, coupant le service pour tous les autres clients en même temps. recover(), utilisé dans un middleware, isole cette panique pour qu'elle n'affecte qu'une seule requête.
D'abord, il faut comprendre l'ordre d'exécution de defer
Chaque appel defer empile une instruction à exécuter plus tard, au moment où la fonction englobante se termine — quelle que soit la façon dont elle se termine (return normal, panic, ou fin naturelle du bloc). Plusieurs defer s'exécutent dans l'ordre inverse de leur déclaration, comme une pile (LIFO) : le dernier defer écrit est le premier exécuté.
Ce mécanisme est le remplaçant Go du traditionnel finally d'autres langages, mais attaché directement au point où la ressource est acquise, ce qui rend le code plus lisible : f.Close() juste après f, err := os.Open(...).
Une fois ce mécanisme acquis, il faut bien distinguer deux façons d'échouer
Go a deux mécanismes distincts pour signaler un problème, avec des usages très différents.
| Mécanisme | Usage prévu | Comment le traiter |
|---|---|---|
error (valeur retournée) | Échec attendu et normal (fichier absent, division par zéro) | if err != nil { ... } |
panic | Situation exceptionnelle, bug ou état incohérent | recover() dans un defer, à réserver aux cas extrêmes |
Prérequis
Il faut être à l'aise avec la gestion d'erreurs idiomatique (error, leçon dédiée) pour bien comprendre pourquoi panic reste l'exception et non la règle en Go.
Il reste une règle stricte sur l'usage de panic
Contrairement à une exception en Java ou Python, qui sert couramment au flux de contrôle normal (par exemple pour signaler qu'une ressource n'existe pas), panic doit rester réservé aux situations réellement exceptionnelles : une erreur de programmation, un état interne incohérent qui ne devrait jamais se produire. Un fichier manquant, une division par zéro attendue, une entrée utilisateur invalide relèvent tous d'error, jamais de panic.
Piège fréquent
recover() ne fonctionne QUE s'il est appelé directement à l'intérieur d'une fonction différée par defer. L'appeler ailleurs dans le code, même juste avant l'endroit où la panique pourrait survenir, ne rattrape rien du tout et laisse le programme planter normalement.
Enfin, un defer peut modifier une valeur de retour nommée
C'est une technique avancée mais très utile : une fonction avec des valeurs de retour nommées peut, dans son defer, appeler recover() et affecter le résultat de cette récupération à la variable de retour nommée err. C'est exactement ce qui permet à une fonction de transformer une panique interne en un simple error propre pour l'appelant.
Bonne pratique
Place systématiquement un recover() au niveau du middleware racine d'un serveur HTTP, pour qu'une panique isolée dans un seul handler ne fasse jamais tomber tout le processus. C'est un filet de sécurité, pas une excuse pour ignorer la gestion d'erreurs normale par ailleurs.
Maintenant que tu sais gérer les cas exceptionnels, la suite du cours attaque des sujets de production avancés, en commençant par un outil indispensable pour diagnostiquer les problèmes de performance réels : le profiling avec pprof.
Commandes & code
Defer, panic et recover
Go n'a pas de try/catch : panic interrompt le flux, recover le rattrape, defer garantit un nettoyage.
package main
import (
"fmt"
"os"
)
func exempleDefer() {
fmt.Println("debut")
defer fmt.Println("1er defer (execute en dernier)")
defer fmt.Println("2e defer (execute avant le 1er)")
defer fmt.Println("3e defer (execute en premier)")
fmt.Println("fin")
// Ordre d'affichage : debut, fin, 3e, 2e, 1er -> LIFO (pile)
}
func ouvrirEtLire(chemin string) error {
f, err := os.Open(chemin)
if err != nil {
return err
}
defer f.Close() // garantit la fermeture, meme si un return anticipe survient plus bas
buf := make([]byte, 100)
_, err = f.Read(buf)
return err // f.Close() s'execute automatiquement ici
}
// defer peut modifier une valeur de retour nommee : utile pour le nettoyage + logging d'erreur
func operationRisquee() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panique recuperee: %v", r)
}
}()
panic("quelque chose s'est mal passe")
}
func diviser(a, b int) (resultat int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recuperation apres panique: %v", r)
}
}()
resultat = a / b // panique si b == 0 : "integer divide by zero"
return
}
// Middleware HTTP typique : recover empeche un seul crash de faire tomber tout le serveur
func recupererPanique(f func()) {
defer func() {
if r := recover(); r != nil {
fmt.Println("panique interceptee, le programme continue:", r)
}
}()
f()
}
func main() {
exempleDefer()
err := operationRisquee()
fmt.Println("erreur:", err)
resultat, err := diviser(10, 0)
fmt.Println(resultat, err)
recupererPanique(func() {
var s []int
_ = s[10] // panic: index out of range
})
fmt.Println("le programme continue normalement apres la panique recuperee")
// panic() non recupere fait planter tout le programme :
// panic("erreur fatale non geree") // decommenter pour voir le crash + stack trace
}Résumé
deferempile les appels (ordre LIFO) et s'exécute même après unpanicou unreturnanticipé.panicdoit rester exceptionnel : pas un substitut àerrorpour le flux normal.recover()ne fonctionne que dans une fonction appelée directement pardefer.- Un serveur HTTP robuste
recover()par requête pour qu'une panique n'affecte qu'un seul client.
Exercices pratiques
Mission : un serveur protégé par recover() qui plante quand même
Objectif : Comprendre pourquoi un recover() de middleware ne protège pas une goroutine annexe, et raisonner sur l'ordre d'exécution de plusieurs recover().
Contexte
Le serveur enveloppe chaque handler avec recupererPanique, qui contient un defer/recover() censé isoler toute panique. Le handler POST /commandes lance en plus une goroutine annexe (go envoyerConfirmation(commande)) pour ne pas ralentir la réponse HTTP. La semaine dernière, une commande mal formée a fait planter tout le processus serveur, alors que le middleware recupererPanique semblait pourtant bien en place.