Por qué panic y recover no son try/catch en Go

En Go, panic no es una excepción de control de flujo y recover no es un bloque catch. Mientras que en lenguajes como Java o Python las excepciones se utilizan para manejar errores esperados (como un archivo no encontrado o un input inválido), en Go un panic representa un error irrecuperable: una violación del contrato del programa (como un índice fuera de rango o un puntero nulo donde no debería existir uno).

Cuando ocurre un panic, el runtime inicia un proceso de desapilado de la pila (stack unwinding). Esto significa que la ejecución de la función actual se detiene, se ejecutan todas las funciones programadas mediante defer en orden inverso (LIFO) y el pánico se propaga hacia arriba, a la función llamante, repitiendo el proceso hasta que llega al nivel más alto. Si nadie intercepta el pánico, el proceso completo termina abruptamente.

La gran diferencia con el modelo try/catch es el alcance. En Go, un recover solo funciona si se llama dentro de un defer dentro de la misma goroutine que originó el pánico. Si lanzas una goroutine con go func() { panic(...) }() y tienes un recover en la goroutine principal, tu programa morirá de todos modos. Por eso, usar panic para lógica de negocio es un error de diseño: hace que el flujo de control sea implícito, oculto y, lo más peligroso, puede causar cierres inesperados que son imposibles de capturar desde fuera de la goroutine que falló.

package main

import (
	"fmt"
	"runtime/debug"
)

// Task representa una unidad de trabajo.
type Task func() error

// SafeRunner ejecuta una tarea en una goroutine nueva pero protege el 
// proceso contra panics internos para que el servidor no se caiga.
// Este es el único escenario donde recover es aceptable: establecer un 
// límite de seguridad (boundary) en una frontera de ejecución.
func SafeRunner(t Task) (err error) {
	done := make(chan error)

	go func() {
		// El recover DEBE estar dentro de la goroutine que puede entrar en panic.
		// Si intentamos poner este defer en SafeRunner (fuera de la goroutine),
		// el panic de la goroutine hija mataría todo el programa.
		defer func() {
			if r := recover(); r != nil {
				fmt.Printf("[Límite de Seguridad] Capturado panic interno: %v\n", r)
				// Imprimimos el stack para depuración, algo vital en producción.
				fmt.Printf("Stack trace: %s\n", debug.Stack())
				err = fmt.Errorf("la tarea falló catastróficamente: %v", r)
				done <- err
			}
		}()

		err = t()
		done <- err
	}()

	return <-done
}

func main() {
	// Caso 1: Error esperado (El camino correcto en Go)
	fmt.Println("--- Caso 1: Error controlado ---")
	err := func() error {
		// En lugar de panic, retornamos un error normal.
		return fmt.Errorf("archivo no encontrado")
	}()
	if err != nil {
		fmt.Printf("Resultado esperado: %v\n", err)
	}

	// Caso 2: Panic contenido en un límite de goroutine (Uso correcto de recover)
	fmt.Println("\n--- Caso 2: Panic protegido en goroutine ---")
	tareaPeligrosa := func() error {
		// Simulamos un error de lógica que causa un panic
		panic("índice fuera de rango (simulado)")
	}

	err = SafeRunner(tareaPeligrosa)
	if err != nil {
		fmt.Printf("El SafeRunner capturó el desastre: %v\n", err)
	}

	// Caso 3: El error fatal (Panic en goroutine sin protección)
	fmt.Println("\n--- Caso 3: El error que mata el programa ---")
	fmt.Println("Lanzando goroutine sin protección... El programa morirá en un segundo.")
	
	go func() {
		panic("¡BOOM! Este panic no tiene recover en su goroutine y matará todo.")
	}()

	// Este código nunca se ejecutará porque el panic de arriba es fatal.
	fmt.Println("Esto no se imprimirá.")
}

Desglose del código

En el SafeRunner, observamos la implementación correcta de un límite de seguridad. La función utiliza una goroutine para ejecutar la tarea t. Dentro de esa goroutine, registramos un defer que contiene la llamada a recover(). Si t() provoca un pánico, la pila se desapila, el defer se ejecuta, recover() captura el valor del pánico y lo asigna a la variable err, permitiendo que la goroutine termine de forma controlada enviando el error al canal done.

Fíjate en la importancia de err = fmt.Errorf(...) dentro del defer. Para que recover pueda devolver un valor a la función llamante, la función debe estar definida con un valor de retorno nombrado (en este caso (err error)). Si no fuera un valor nombrado, el cambio en err no se propagaría hacia afuera de la función.

El main ilustra la diferencia crítica: en el Caso 2, el panic es capturado por el SafeRunner porque el recover reside en la misma goroutine que el error. Sin embargo, en el Caso 3, el panic ocurre en una goroutine huérfana. Aunque main intentara capturar el error, el runtime de Go no permite que un pánico de una goroutine sea capturado por otra; el sistema operativo simplemente recibe la señal de salida del proceso.

El error frecuente

El error más peligroso en Go es asumir que un recover en la función principal o en una función “padre” puede salvarte de un error en una goroutine hija.

// ERROR FATAL: El recover en la función padre es inútil para goroutines hijas.
func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Intentando capturar el pánico...")
        }
    }()

    go func() {
        panic("Este pánico matará el proceso completo a pesar del defer de arriba")
    }()

    // El programa termina aquí con un crash, el recover de arriba no se ejecuta.
    select {} 
}

Este patrón suele ocurrir cuando se intenta usar Go como si fuera un lenguaje con excepciones tradicionales, olvidando que la jerarquía de panics está ligada estrictamente a la jerarquía de la pila de ejecución de cada goroutine individual.

95

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio