À quoi ressemble un panic
Un panic arrête la fonction courante, exécute ses appels différés, puis fait de même dans son appelant, et ainsi de suite en remontant la pile. S'il atteint le sommet de la goroutine, le programme plante.
Ce programme sort avec le statut 2. La sortie est :
before
deferred in main: still runs
panic: runtime error: index out of range [5] with length 3
goroutine 1 [running]:
main.main()
/tmp/main.go:11 +0x...
exit status 2
L'appel différé s'est exécuté avant le rapport de plantage. La trace nomme la goroutine, la fonction et la ligne, ce qui suffit généralement à trouver le bug.
Les panics d'exécution courants
| Message | Cause |
|---|---|
index out of range [5] with length 3 | indice de slice, de tableau ou de chaîne au-delà de la fin |
slice bounds out of range [:7] with capacity 5 | découpage au-delà de la capacité |
invalid memory address or nil pointer dereference | lecture d'un champ ou appel via un pointeur nil |
assignment to entry in nil map | écriture dans une map jamais créée |
interface conversion: interface {} is int, not string | assertion de type à une seule valeur vers le mauvais type |
integer divide by zero | division entière ou modulo par 0 (les flottants donnent +Inf ou NaN à la place) |
close of closed channel, send on closed channel | mauvaise utilisation des channels |
all goroutines are asleep - deadlock! | toutes les goroutines bloquées (une erreur fatale, pas un panic) |
Chacun de ces cas est un bug du programme, pas une condition à gérer. La correction est une vérification des bornes, un test de nil, un make ou une assertion comma-ok, pas un recover.
Récupérer
recover() arrête un panic. Il ne fonctionne que s'il est appelé directement dans une fonction différée, parce que les fonctions différées sont le seul code qui s'exécute pendant le déroulement d'un panic.
Sortie :
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
Ce qui s'est passé lors du second appel :
a / ba provoqué un panic.- La closure différée s'est exécutée, et
recover()a renvoyé la valeur du panic (uneruntime.Error). - Le déroulement s'est arrêté.
safeDivideest retournée normalement àmain, avec le résultat nomméerrdéfini par la closure.
C'est le résultat nommé qui permet à la fonction différée de renvoyer une erreur. Sans lui, la fonction renvoie ses valeurs zéro. La page defer explique comment les closures différées modifient les résultats.
recover() renvoie nil en l'absence de panic, donc le test if r != nil rend la fonction différée inoffensive sur le chemin normal. Appelé hors d'une fonction différée, ou dans une fonction appelée par la fonction différée, recover renvoie nil et ne fait rien.
panic avec votre propre valeur
panic accepte n'importe quelle valeur. Une erreur ou une chaîne est typique.
Récupérez ce que vous attendez et relancez le panic pour tout le reste. Avaler tous les panics masque de vrais bugs.
Depuis Go 1.21, panic(nil) est transformé en *runtime.PanicNilError, donc un recover() qui renvoie nil signifie désormais de façon fiable « pas de panic ».
Panics dans les goroutines
recover n'attrape que les panics de sa propre goroutine. Un panic dans n'importe quelle goroutine sans recover tue tout le processus, y compris main et toutes les autres goroutines.
Les deux lignes des workers peuvent s'afficher dans n'importe quel ordre ; main finished vient toujours en dernier. Un defer recover() dans main n'aurait pas sauvé le programme du second worker. C'est pourquoi les serveurs HTTP récupèrent requête par requête : net/http récupère les panics dans la goroutine de chaque handler, les journalise et ferme cette connexion, pour qu'une mauvaise requête ne fasse pas tomber le serveur.
Certains échecs sont des erreurs fatales, pas des panics, et ne peuvent pas être récupérés du tout : concurrent map writes, le manque de mémoire, et le all goroutines are asleep du détecteur de deadlocks.
Quand paniquer est le bon choix
La règle générale de Go : renvoyez des erreurs pour tout ce qui peut mal tourner à l'exécution, et ne paniquez que pour les erreurs du programmeur. Concrètement, paniquer est approprié quand :
- Un invariant est cassé. Un
switchsur votre propre enum atteint un cas qui ne peut pas se produire. Continuer corromprait des données. - Un utilitaire
Mustreçoit une entrée constante invalide.regexp.MustCompile,template.Mustetuuid.MustParseenveloppent une fonction qui renvoie une erreur et paniquent en cas d'échec. Utilisez-les pour des valeurs connues à la compilation, typiquement des variables au niveau du package, où un échec signifie que le code source est faux :
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- Le démarrage ne peut pas continuer. Une configuration obligatoire manque dans
main. Même là, afficher l'erreur et appeleros.Exit(1)est souvent plus propre qu'une trace de pile.
Panic n'est pas le bon outil pour :
- Les échecs prévus : une saisie utilisateur invalide, un fichier manquant, un timeout. Renvoyez une
error; voir la gestion des erreurs. - Le flux de contrôle : utiliser panic et recover comme des exceptions à travers un grand arbre d'appels rend le code difficile à suivre. La bibliothèque standard le fait en interne à quelques endroits (l'encodeur de
encoding/json), en récupérant toujours avant de retourner, pour qu'aucun panic ne s'échappe du package. - Les API de bibliothèque : une bibliothèque qui panique sur une entrée invalide oblige chaque appelant à ajouter des recover. Renvoyez une erreur.
Erreurs courantes
- Appeler recover hors d'une fonction différée. Il renvoie
nil. - Récupérer dans
mainle panic d'une goroutine. Chaque goroutine a besoin du sien. - Avaler tous les panics. Journalisez avec la pile (
debug.Stack()deruntime/debug) et relancez le panic pour ce que vous n'attendiez pas. - Utiliser
recoverpour gérer des écritures dans une map nil ou des indices hors limites. Corrigez plutôt le bug.
Questions fréquentes
Qu'est-ce qu'un panic en Go ?
Un échec à l'exécution qui arrête le flux normal de la goroutine courante. Go exécute les appels différés de chaque fonction de la pile, de la plus interne vers l'extérieur, et si rien ne le récupère, le programme affiche la valeur du panic et une trace de pile, puis sort avec le statut 2. Les panics viennent de bugs (indice hors limites, déréférencement de pointeur nil, écriture dans une map nil) ou d'un appel explicite à panic(v).
Comment récupérer un panic en Go ?
Appelez recover() dans une fonction différée : defer func() { if r := recover(); r != nil { ... } }(). Il renvoie la valeur passée à panic et arrête le déroulement, donc la fonction qui l'a différée retourne normalement à son appelant. Appelé n'importe où ailleurs, recover renvoie nil et ne fait rien.
Peut-on récupérer un panic depuis une autre goroutine ?
Non. recover n'arrête qu'un panic de la goroutine où il s'exécute. Un panic dans une goroutine que vous avez lancée, sans recover dans cette goroutine, fait planter tout le programme. Chaque goroutine susceptible de paniquer a besoin de son propre recover différé.
Quand utiliser panic plutôt que renvoyer une erreur ?
Pour les bugs et les états impossibles, pas pour les échecs prévus. Une entrée invalide, un fichier manquant ou une erreur réseau sont des erreurs. Paniquer est raisonnable quand un invariant est cassé, quand un utilitaire Must reçoit une constante qui devrait toujours être valide (regexp.MustCompile), ou quand le programme ne peut pas démarrer du tout. Les bibliothèques ne devraient pas laisser des panics s'échapper de leur API publique.