Menu

Panic et recover en Go : panics d'exécution et quand paniquer

Un panic arrête l'exécution normale et déroule la pile en exécutant les appels différés. Découvrez ce qui provoque les panics, comment recover dans une fonction différée en arrête un, les messages d'erreur d'exécution que vous verrez, et quand paniquer est le bon choix.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

À 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

MessageCause
index out of range [5] with length 3indice de slice, de tableau ou de chaîne au-delà de la fin
slice bounds out of range [:7] with capacity 5découpage au-delà de la capacité
invalid memory address or nil pointer dereferencelecture 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 stringassertion de type à une seule valeur vers le mauvais type
integer divide by zerodivision entière ou modulo par 0 (les flottants donnent +Inf ou NaN à la place)
close of closed channel, send on closed channelmauvaise 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 :

  1. a / b a provoqué un panic.
  2. La closure différée s'est exécutée, et recover() a renvoyé la valeur du panic (une runtime.Error).
  3. Le déroulement s'est arrêté. safeDivide est retournée normalement à main, avec le résultat nommé err dé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 switch sur votre propre enum atteint un cas qui ne peut pas se produire. Continuer corromprait des données.
  • Un utilitaire Must reçoit une entrée constante invalide. regexp.MustCompile, template.Must et uuid.MustParse enveloppent 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 appeler os.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 main le panic d'une goroutine. Chaque goroutine a besoin du sien.
  • Avaler tous les panics. Journalisez avec la pile (debug.Stack() de runtime/debug) et relancez le panic pour ce que vous n'attendiez pas.
  • Utiliser recover pour 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.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER