Patrón de frontera con recover en servidores Go

recover() es una función incorporada que permite retomar el control de una goroutine cuando ocurre un panic. Su propósito es detener el proceso de “unwinding” de la pila (el vaciado de la pila de llamadas) y permitir que la ejecución continúe tras el bloque defer donde fue invocada. Esta restricción es fundamental por diseño: recover() solo tiene efecto si se llama directamente dentro de una función diferida; de lo contrario, simplemente devolverá nil sin hacer nada. En sistemas de alta disponibilidad, no queremos que un error inesperado en una petición HTTP —como un acceso a un índice de un slice fuera de rango— derribe todo el binario. Por eso, implementamos un “patrón de frontera” (boundary pattern) en los puntos de entrada de cada goroutine crítica para capturar errores catastróficos y mantener el proceso vivo. Sin embargo, si se usa de forma indiscriminada, corres un riesgo real: si un panic ocurre a mitad de una operación que modifica una estructura de datos compartida, esa estructura puede quedar en un estado inconsistente o corrupto, provocando errores lógicos o nuevos panics impredecibles más adelante.

package main

import (
	"fmt"
	"log"
	"net/http"
	"runtime/debug"
)

// unsafeHandler simula una función que realiza operaciones críticas
func unsafeHandler(w http.ResponseWriter, r *http.Request) {
	// El patrón de frontera: capturamos el panic para que la goroutine
	// del servidor HTTP no muera por completo y el servidor siga operando.
	defer func() {
		if err := recover(); err != nil {
			// 1. Logueamos el error y el stack trace completo.
			// Es vital usar debug.Stack() porque el valor de err 
			// suele ser muy escueto (ej: "runtime error: index out of range").
			log.Printf("RECOVERED PANIC: %v\n%s", err, debug.Stack())

			// 2. Respondemos al cliente con un error 500 genérico.
			// Nunca expongas detalles del panic al cliente por seguridad.
			http.Error(w, "Internal Server Error", http.StatusInternalServerError)
		}
	}()

	// Simulación de un error crítico basado en un parámetro de la URL
	if r.URL.Query().Get("fail") == "true" {
		fmt.Println("Iniciando operación fallida...")
		var s []string
		// Esto disparará un panic: runtime error: index out of range
		fmt.Println(s[5]) 
	}

	fmt.Fprintf(w, "Operación exitosa")
}

func main() {
	http.HandleFunc("/test", unsafeHandler)

	fmt.Println("Servidor corriendo en :8080. Prueba con: curl 'http://localhost:8080/test?fail=true'")
	
	// ListenAndServe levanta el servidor. Si unsafeHandler no tuviera
	// el defer/recover, un panic en una petición mataría este proceso.
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatalf("Error crítico al iniciar el servidor: %v", err)
	}
}

En el ejemplo anterior, unsafeHandler actúa como nuestra frontera. Cuando el cliente realiza una petición con el parámetro fail=true, el programa intenta acceder a un índice inexistente en un slice vacío, lo que lanza un panic.

En ese momento, la ejecución normal se interrumpe y Go comienza a ejecutar las funciones defer de la pila actual hacia atrás. El defer definido dentro de unsafeHandler se activa. Al llamar a recover(), la función intercepta el valor del panic. Mediante la instrucción if err := recover(); err != nil, capturamos la información del error.

Es crucial observar el uso de debug.Stack(). En un entorno de producción, un recover sin el stack trace es casi inútil, ya que solo sabrías que algo falló, pero no en qué línea de código ocurrió el desastre. Finalmente, usamos http.Error para informar al cliente que algo salió mal de forma controlada, evitando que la conexión se quede colgada o que el cliente reciba una respuesta vacía sin contexto.

El error frecuente

Un error muy común es intentar llamar a recover() en una función normal, pensando que puede “atrapar” un panic que venga de una función llamada anteriormente.

func worker() {
	panic("error catastrófico")
}

func attemptRecovery() {
	// Error: recover() no está dentro de un defer.
	// No capturará el panic de la goroutine.
	recover() 
	fmt.Println("Esto nunca se imprimirá si worker() lanza un panic")
}

func main() {
	go worker()
	go attemptRecovery()
	
	// El programa colapsará. recover() solo funciona si se llama
	// directamente dentro de una función diferida (defer) durante
	// el proceso de desapilado (unwinding) de la pila.
}

Para que recover() sea efectivo, debe ejecutarse en la misma goroutine que sufrió el panic y debe ser invocado mediante defer para que el runtime de Go le dé la oportunidad de interceptar el flujo antes de que el proceso termine.

94

Dejar un comentario

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

Scroll al inicio