panic
panic est une fonction intégrée de Go. Lorsqu'une erreur irrécupérable se produit, le programme lance souvent une panic, comme par exemple l'accès à un pointeur nil :
func main() {
var a *int
*a = 1
}En exécutant ce code, le programme lance la panic suivante, puis s'arrête :
panic: runtime error: invalid memory address or nil pointer dereferenceDans certains cas, nous appelons manuellement la fonction panic pour faire sortir le programme et éviter des conséquences plus graves. On utilise aussi souvent une autre fonction intégrée recover pour capturer une panic, en combinaison avec defer :
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var a *int
*a = 1
}Pourquoi la fonction recover doit-elle être utilisée dans un defer ? Que fait exactement recover ? Ces questions trouveront leur réponse dans la suite.
Structure
panic est représentée à l'exécution par une structure runtime._panic, dont la structure n'est pas complexe :
type _panic struct {
argp unsafe.Pointer // pointer to arguments of deferred call run during panic; cannot move - known to liblink
arg any // argument to panic
link *_panic // link to earlier panic
pc uintptr // where to return to in runtime if this panic is bypassed
sp unsafe.Pointer // where to return to in runtime if this panic is bypassed
recovered bool // whether this panic is over
aborted bool // the panic was aborted
goexit bool
}Sa structure est très similaire à celle de defer :
linkpointe vers la prochaine structure_panicpcetsppointent vers le contexte d'exécution de la fonction appelante pour permettre une restauration ultérieureargest l'argument de la fonctionpanicargppointe vers les arguments dudefer, car l'exécution dudeferest déclenchée lorsqu'unepanicse produitabortedindique si elle a été arrêtée de force
Comme defer, panic existe sous forme de liste chaînée dans la goroutine :
type g struct {
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
}
Panique
Que ce soit un appel manuel à panic ou une panic déclenchée par le programme, on finit toujours par entrer dans la fonction runtime.gopanic :
func gopanic(e any)Au début, on vérifie si l'argument est nil. Si c'est le cas, on crée une erreur de type runtime.PanicNilError :
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}Ensuite, on ajoute la panic courante en tête de la liste chaînée de la goroutine :
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))Puis on entre dans une boucle for pour traiter un par un les defer de la liste chaînée de la goroutine courante :
for {
d := gp._defer
if d == nil {
break
}
if d.started {
if d._panic != nil {
d._panic.aborted = true
}
d._panic = nil
}
d.started = true
d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
...
}Si le defer courant a déjà été déclenché par une autre panic, c'est-à-dire _defer.started == true, alors la panic précédente ne sera pas exécutée. Ensuite, on exécute la fonction associée au defer :
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)Après l'exécution, on libère la structure defer courante et on continue avec le prochain defer. Quand tous les defer ont été exécutés sans qu'il y ait eu de récupération, on entre dans la fonction runtime.fatalpanic, qui est unrecoverable c'est-à-dire irrécupérable :
func fatalpanic(msgs *_panic) {
pc := getcallerpc()
sp := getcallersp()
gp := getg()
var docrash bool
systemstack(func() {
if startpanic_m() && msgs != nil {
runningPanicDefers.Add(-1)
printpanics(msgs)
}
docrash = dopanic_m(gp, pc, sp)
})
if docrash {
crash()
}
systemstack(func() {
exit(2)
})
*(*int)(nil) = 0 // not reached
}Pendant ce temps, printpanics affiche les informations de la panic. C'est cette fonction qui produit les traces d'appel que nous voyons habituellement. Enfin, la fonction runtime.exit termine le programme via l'appel système _ExitProcess.
Récupération
En appelant la fonction intégrée recover, le compilateur la convertit en un appel à la fonction runtime.gorecover :
func gorecover(argp uintptr) any {
gp := getg()
p := gp._panic
if p != nil && !p.goexit && !p.recovered && argp == uintptr(p.argp) {
p.recovered = true
return p.arg
}
return nil
}Son implémentation est très simple, elle ne fait que p.recovered = true. La vraie logique de récupération se trouve en fait dans la fonction gopanic :
for {
...
d.fn()
...
if p.recovered {
...
}
}La logique de récupération s'exécute après l'exécution du defer. On comprend maintenant pourquoi la fonction recover ne peut être utilisée que dans un defer. Si on utilise recover en dehors d'un defer, gp._panic vaudra nil, donc p.recovered ne sera pas mis à true, et dans la fonction gopanic on n'entrera pas dans la logique de récupération.
if p.recovered {
gp._panic = p.link
for gp._panic != nil && gp._panic.aborted {
gp._panic = gp._panic.link
}
if gp._panic == nil {
gp.sig = 0
}
gp.sigcode0 = uintptr(sp)
gp.sigcode1 = pc
mcall(recovery)
throw("recovery failed")
}Lors de la récupération, on nettoie les panic qui ont été arrêtées de force dans la liste chaînée, puis on entre dans la fonction runtime.recovery, qui utilise runtime.gogo pour revenir au flux normal de la fonction utilisateur :
func recovery(gp *g) {
// Info about defer passed in G struct.
sp := gp.sigcode0
pc := gp.sigcode1
gp.sched.sp = sp
gp.sched.pc = pc
gp.sched.lr = 0
gp.sched.ret = 1
gogo(&gp.sched)
}Un point important à noter est cette ligne de code :
gp.sched.ret = 1Elle met la valeur ret à 1. Dans le commentaire de la fonction runtime.deferproc, on peut voir ceci :
func deferproc(fn func()) {
...
// deferproc returns 0 normally.
// a deferred func that stops a panic
// makes the deferproc return 1.
// the code the compiler generates always
// checks the return value and jumps to the
// end of the function if deferproc returns != 0.
return0()
}Le code intermédiaire généré par le compilateur vérifie si cette valeur vaut 1. Si c'est le cas, il exécute directement la fonction runtime.deferreturn. Normalement, cette fonction n'est exécutée qu'avant le retour de la fonction, ce qui explique pourquoi après un recover, la fonction retourne directement.
