panic
panic es una función incorporada en Go. Cuando se encuentran errores irrecoverables, el programa a menudo lanzará un panic, como el acceso común a puntero nulo:
func main() {
var a *int
*a = 1
}Al ejecutar el código anterior, el programa lanzará el siguiente panic y luego se detendrá:
panic: runtime error: invalid memory address or nil pointer dereferenceEn algunas situaciones, también llamamos manualmente a la función panic para hacer que el programa termine, evitando así consecuencias más graves. También usamos otra función incorporada recover para capturar panic, y la usamos junto con defer.
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
var a *int
*a = 1
}¿Por qué la función recover debe usarse dentro de defer? ¿Qué trabajo hace recover? Estas preguntas se responderán en el contenido a continuación.
Estructura
panic también tiene una estructura correspondiente en tiempo de ejecución, que es runtime._panic. Su estructura no es compleja, como sigue:
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
}Su estructura es muy similar a defer:
linkapunta a la siguiente estructura_panicpcyspapuntan al contexto de ejecución de la función llamadora para facilitar la recuperación posteriorarges el parámetro de la funciónpanicargpapunta a los parámetros dedefer. Cuando ocurrepanic, se activa la ejecución dedeferabortedindica si fue detenido forzosamente
panic, al igual que defer, también existe como una lista enlazada en la goroutine:
type g struct {
_panic *_panic // innermost panic - offset known to liblink
_defer *_defer // innermost defer
}
Pánico
Tanto si llamamos activamente a la función panic como si el programa produce un panic, finalmente se entrará en la función runtime.gopanic:
func gopanic(e any)Al inicio, primero se detecta si el parámetro es nil. Si es nil, se creará un error de tipo runtime.PanicNilError:
if e == nil {
if debug.panicnil.Load() != 1 {
e = new(PanicNilError)
} else {
panicnil.IncNonDefault()
}
}Luego se agrega el panic actual a la cabeza de la lista enlazada de la goroutine:
var p _panic
p.arg = e
p.link = gp._panic
gp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))Después se entra en un bucle for para comenzar a procesar uno por uno la lista enlazada defer de la goroutine actual:
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 el defer actual ya fue activado por otro panic, es decir, _defer.started == true, entonces el panic anterior no se ejecutará. Luego se ejecuta la función correspondiente a defer:
p.argp = unsafe.Pointer(getargp())
d.fn()
p.argp = nil
d._panic = nil
d.fn = nil
gp._defer = d.link
freedefer(d)Después de ejecutar, se recicla la estructura defer actual y se continúa ejecutando el siguiente defer. Cuando se ejecutan todas las estructuras defer y no se recuperó durante el proceso, se entrará en la función runtime.fatalpanic. Esta función es unrecoverable, es decir, irrecoverable:
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
}Durante este proceso, printpanics imprimirá la información de panic. La información de la pila de llamadas que normalmente vemos es producida por esta función. Finalmente, la función runtime.exit sale del programa mediante la llamada al sistema _ExitProcess.
Recuperación
Al llamar a la función incorporada recover, durante la compilación se convierte en una llamada a la función 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
}Su implementación es muy simple, solo hace una cosa: p.recovered = true. El código que realmente maneja la lógica de recuperación está en la función gopanic:
for {
...
d.fn()
...
if p.recovered {
...
}
}La lógica de recuperación está después de que se ejecuta defer. Aquí se entiende por qué la función recover solo puede usarse en defer. Si se usa recover fuera de defer, entonces gp._panic sería nil, y naturalmente p.recovered no se establecería en true. Entonces, en la función gopanic, no se entraría en la parte de lógica de recuperación.
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")
}Durante la recuperación, se limpian aquellos panic en la lista enlazada que ya fueron detenidos forzosamente. Luego se entra en la función runtime.recovery, y mediante runtime.gogo se vuelve al flujo de lógica normal de la función de usuario:
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 punto importante a tener en cuenta es esta línea de código:
gp.sched.ret = 1Establece el valor ret en 1. En los comentarios de la función runtime.deferproc se puede ver el siguiente contenido:
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()
}El código intermedio generado por el compilador verificará si este valor es 1. Si es así, ejecutará directamente la función runtime.deferreturn. Normalmente esta función solo se ejecuta antes de que la función retorne. Esto también explica por qué después de recover la función retorna directamente.
