Qué aspecto tiene un panic
Un panic detiene la función actual, ejecuta sus llamadas diferidas, luego hace lo mismo en la función que la llamó, y así sucesivamente hacia arriba por la pila. Si llega a la parte alta de la goroutine, el programa cae.
Este programa sale con estado 2. La salida es:
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
La llamada diferida se ejecutó antes del informe de la caída. La traza nombra la goroutine, la función y la línea, lo que suele bastar para encontrar el bug.
Panics habituales del runtime
| Mensaje | Causa |
|---|---|
index out of range [5] with length 3 | índice de un slice, un array o un string más allá del final |
slice bounds out of range [:7] with capacity 5 | recortar más allá de la capacidad |
invalid memory address or nil pointer dereference | leer un campo o llamar a través de un puntero nil |
assignment to entry in nil map | escribir en un map que nunca se creó |
interface conversion: interface {} is int, not string | aserción de tipo de un solo valor con el tipo equivocado |
integer divide by zero | división entera o módulo entre 0 (los flotantes dan +Inf o NaN) |
close of closed channel, send on closed channel | mal uso de un channel |
all goroutines are asleep - deadlock! | todas las goroutines bloqueadas (un error fatal, no un panic) |
Cada uno de ellos es un bug del programa, no una condición que haya que manejar. El arreglo es una comprobación de límites, una comprobación de nil, un make o una aserción comma-ok, no un recover.
Recuperar
recover() detiene un panic. Solo funciona cuando se llama directamente dentro de una función diferida, porque las funciones diferidas son el único código que se ejecuta mientras un panic deshace la pila.
Salida:
5 <nil>
0 recovered: runtime error: integer divide by zero
program continues
Qué pasó en la segunda llamada:
a / bprovocó un panic.- Se ejecutó la closure diferida, y
recover()devolvió el valor del panic (unruntime.Error). - Se detuvo el deshacer de la pila.
safeDivideretornó con normalidad amain, con el resultado con nombreerrasignado por la closure.
El resultado con nombre es lo que permite a la función diferida devolver un error. Sin él, la función devuelve sus valores cero. La página de defer explica cómo las closures diferidas modifican los resultados.
recover() devuelve nil cuando no hay panic, así que la comprobación if r != nil hace que la función diferida sea inofensiva en la ruta normal. Llamado fuera de una función diferida, o en una función llamada por la función diferida, recover devuelve nil y no hace nada.
panic con tu propio valor
panic acepta cualquier valor. Lo típico es un error o un string.
Recupera lo que esperas y vuelve a provocar el panic con todo lo demás. Tragarse todos los panics esconde bugs reales.
Desde Go 1.21, panic(nil) se convierte en un *runtime.PanicNilError, así que ahora que recover() devuelva nil significa con seguridad "no hubo panic".
Panics en goroutines
recover solo captura los panics de su propia goroutine. Un panic en cualquier goroutine sin recover mata el proceso entero, incluidos main y todas las demás goroutines.
Las dos líneas de los workers pueden imprimirse en cualquier orden; main finished siempre sale la última. Un defer recover() en main no habría salvado al programa del segundo worker. Por eso los servidores HTTP recuperan por petición: net/http recupera los panics en la goroutine de cada handler, los registra y cierra esa conexión, para que una petición defectuosa no tumbe el servidor.
Algunos fallos son errores fatales, no panics, y no se pueden recuperar de ninguna forma: concurrent map writes, quedarse sin memoria y el all goroutines are asleep del detector de deadlocks.
Cuándo un panic es la decisión correcta
La regla general de Go: devuelve errores para todo lo que puede ir mal en tiempo de ejecución, y provoca un panic solo ante errores del programador. En concreto, un panic es adecuado cuando:
- Se rompe un invariante. Un
switchsobre tu propio enum llega a un caso que no puede ocurrir. Continuar corrompería datos. - Una función auxiliar
Mustrecibe una entrada constante incorrecta.regexp.MustCompile,template.Mustyuuid.MustParseenvuelven una función que devuelve un error y provocan un panic si falla. Úsalas para valores conocidos en tiempo de compilación, normalmente variables a nivel de paquete, donde un fallo significa que el código fuente está mal:
var emailRE = regexp.MustCompile(`^[^@\s]+@[^@\s]+$`)
- El arranque no puede continuar. Falta configuración obligatoria en
main. Incluso aquí, imprimir el error y llamar aos.Exit(1)suele quedar más limpio que una traza de la pila.
Un panic es la herramienta equivocada para:
- Fallos esperados: entrada de usuario no válida, un archivo que falta, un timeout. Devuelve un
error; consulta manejo de errores. - Flujo de control: usar panic y recover como excepciones a lo largo de un árbol de llamadas grande hace el código difícil de seguir. La librería estándar lo hace internamente en un par de sitios (el encoder de
encoding/json), siempre recuperando antes de retornar, así que ningún panic sale del paquete. - APIs de librerías: una librería que provoca un panic ante una entrada incorrecta obliga a cada llamador a añadir recovers. Devuelve un error.
Errores comunes
- Llamar a recover fuera de una función diferida. Devuelve
nil. - Recuperar en
mainel panic de una goroutine. Cada goroutine necesita el suyo. - Tragarse todos los panics. Registra la pila (
debug.Stack()deruntime/debug) y vuelve a provocar el panic con lo que no esperabas. - Usar
recoverpara manejar escrituras en maps nil o índices fuera de rango. Arregla el bug.
Preguntas frecuentes
¿Qué es un panic en Go?
Un fallo en tiempo de ejecución que detiene el flujo normal de la goroutine actual. Go ejecuta las llamadas diferidas de cada función de la pila, de la más interna hacia fuera, y si nada lo recupera, el programa imprime el valor del panic y una traza de la pila y sale con estado 2. Los panics vienen de bugs (índice fuera de rango, desreferencia de un puntero nil, escritura en un map nil) o de una llamada explícita a panic(v).
¿Cómo se recupera un panic en Go?
Llama a recover() dentro de una función diferida: defer func() { if r := recover(); r != nil { ... } }(). Devuelve el valor que se pasó a panic y detiene el deshacer de la pila, así que la función que la difirió retorna con normalidad a quien la llamó. Llamada en cualquier otro sitio, recover devuelve nil y no hace nada.
¿Puedo recuperar un panic de otra goroutine?
No. recover solo detiene un panic en la goroutine donde se ejecuta. Un panic en una goroutine que tú lanzaste, sin recover dentro de esa goroutine, hace caer todo el programa. Cada goroutine que pueda provocar un panic necesita su propio recover diferido.
¿Cuándo debo usar panic en lugar de devolver un error?
Para bugs y estados imposibles, no para fallos esperados. La entrada incorrecta, los archivos que faltan y los errores de red son errores. Un panic es razonable cuando se rompe un invariante, cuando a una función auxiliar Must se le da una constante que siempre debería ser válida (regexp.MustCompile), o cuando el programa no puede ni arrancar. Las librerías no deberían dejar que los panics escapen por su API pública.