← Volver al listado de tecnologías

Panic, Recover y Defer Avanzado

Por: Artiko
gopanicrecoverdefer

Panic, Recover y Defer Avanzado

Que es panic()

panic detiene la ejecucion normal de la goroutine actual. Ejecuta los defers pendientes y luego termina el programa con un stack trace.

package main

import "fmt"

func main() {
	fmt.Println("inicio")
	panic("algo salio muy mal")
	fmt.Println("esto nunca se ejecuta")
}
// inicio
// panic: algo salio muy mal
// goroutine 1 [running]:
// main.main()
//     main.go:7 +0x...

Cuando SI usar panic

EscenarioEjemplo
Errores de programadorIndice fuera de rango, nil pointer
Estado imposibleUn switch que cubre todos los casos
Inicializacion fallidaNo se puede conectar a un recurso critico al arrancar
Violacion de invariantesPrecondiciones que nunca deberian fallar
func MustParseConfig(path string) Config {
	cfg, err := ParseConfig(path)
	if err != nil {
		panic("config invalida: " + err.Error())
	}
	return cfg
}

La convencion Must en Go indica funciones que hacen panic en vez de retornar error.

Cuando NO usar panic

// MAL: no uses panic para errores esperados
func LeerArchivo(nombre string) []byte {
	data, err := os.ReadFile(nombre)
	if err != nil {
		panic(err) // NO hagas esto
	}
	return data
}

// BIEN: retorna el error
func LeerArchivo(nombre string) ([]byte, error) {
	return os.ReadFile(nombre)
}

Defer basico y avanzado

Orden LIFO

Los defers se ejecutan en orden Last In, First Out (pila):

package main

import "fmt"

func main() {
	fmt.Println("inicio")
	defer fmt.Println("primero en defer")
	defer fmt.Println("segundo en defer")
	defer fmt.Println("tercero en defer")
	fmt.Println("fin")
}
// inicio
// fin
// tercero en defer
// segundo en defer
// primero en defer

Evaluacion inmediata de argumentos

Los argumentos de defer se evaluan en el momento del defer, no cuando se ejecuta:

package main

import "fmt"

func main() {
	x := 10
	defer fmt.Println("defer x:", x) // x=10 se captura aqui
	x = 20
	fmt.Println("x:", x)
}
// x: 20
// defer x: 10

Para capturar el valor final usa un closure:

func main() {
	x := 10
	defer func() {
		fmt.Println("defer x:", x) // captura la referencia
	}()
	x = 20
}
// defer x: 20

Defer en loops: cuidado

Defer en un loop acumula llamadas hasta que la funcion retorna. Esto puede causar fugas de recursos:

// MAL: todos los archivos quedan abiertos hasta que termine el loop
func procesarArchivos(nombres []string) error {
	for _, nombre := range nombres {
		f, err := os.Open(nombre)
		if err != nil {
			return err
		}
		defer f.Close() // se acumula por cada iteracion
		// procesar f...
	}
	return nil
}

// BIEN: extraer a una funcion
func procesarArchivos(nombres []string) error {
	for _, nombre := range nombres {
		if err := procesarUno(nombre); err != nil {
			return err
		}
	}
	return nil
}

func procesarUno(nombre string) error {
	f, err := os.Open(nombre)
	if err != nil {
		return err
	}
	defer f.Close()
	// procesar f...
	return nil
}

Patrones de limpieza con defer

Archivos

func leerConfig(path string) (Config, error) {
	f, err := os.Open(path)
	if err != nil {
		return Config{}, err
	}
	defer f.Close()

	var cfg Config
	err = json.NewDecoder(f).Decode(&cfg)
	return cfg, err
}

Mutex

var mu sync.Mutex
var contador int

func incrementar() {
	mu.Lock()
	defer mu.Unlock()
	contador++
	// cualquier return o panic libera el lock
}

Conexiones de base de datos

func consultar(db *sql.DB, id int) (Usuario, error) {
	rows, err := db.Query("SELECT nombre FROM usuarios WHERE id = ?", id)
	if err != nil {
		return Usuario{}, err
	}
	defer rows.Close()

	var u Usuario
	if rows.Next() {
		err = rows.Scan(&u.Nombre)
	}
	return u, err
}

HTTP response body

func obtenerDatos(url string) ([]byte, error) {
	resp, err := http.Get(url)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	return io.ReadAll(resp.Body)
}

Recover

recover() captura un panic y devuelve el valor pasado a panic(). Solo funciona dentro de una funcion defer.

package main

import "fmt"

func operacionRiesgosa() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recuperado:", r)
		}
	}()
	panic("explosion!")
}

func main() {
	operacionRiesgosa()
	fmt.Println("el programa continua")
}
// recuperado: explosion!
// el programa continua

Recover fuera de defer no funciona

func main() {
	r := recover() // siempre retorna nil fuera de defer
	fmt.Println(r)  // <nil>
	panic("boom")   // no se recupera
}

Patron defer + recover: “try/catch” en Go

Puedes crear un wrapper que convierta panics en errores:

package main

import (
	"fmt"
	"runtime/debug"
)

func safeExecute(fn func()) (err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("panic recuperado: %v", r)
		}
	}()
	fn()
	return nil
}

func main() {
	err := safeExecute(func() {
		panic("operacion fallida")
	})
	if err != nil {
		fmt.Println("Error:", err)
	}

	err = safeExecute(func() {
		fmt.Println("operacion exitosa")
	})
	fmt.Println("Error:", err)
}
// Error: panic recuperado: operacion fallida
// operacion exitosa
// Error: <nil>

Uso real: servidores HTTP

El paquete net/http usa recover internamente para que un panic en un handler no mate el servidor completo:

func miHandler(w http.ResponseWriter, r *http.Request) {
	// si esto hace panic, el servidor sigue corriendo
	// net/http lo recupera y responde 500
	datos := procesarPeticion(r)
	json.NewEncoder(w).Encode(datos)
}

Stack traces

Puedes obtener el stack trace en un recover usando runtime/debug:

package main

import (
	"fmt"
	"runtime/debug"
)

func funcionProfunda() {
	panic("error en las profundidades")
}

func funcionMedia() {
	funcionProfunda()
}

func main() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Panic:", r)
			fmt.Println("Stack trace:")
			debug.PrintStack()
		}
	}()
	funcionMedia()
}

Para obtener el stack como string en vez de imprimirlo:

stack := string(debug.Stack())
log.Printf("panic: %v\nstack: %s", r, stack)

Comparacion con excepciones

AspectoGo (panic/recover)Java/C# (try/catch)Python (try/except)
Uso principalErrores irrecuperablesCualquier errorCualquier error
Flujo normal de erroresReturn errorThrow exceptionsRaise exceptions
Costo de rendimientoMinimo (raro)Overhead en throwOverhead en raise
FilosofiaExplicito, valores de retornoExcepciones como flujoExcepciones como flujo
PropagacionManual (return err)Automatica (stack unwinding)Automatica
Checked exceptionsNo existenSi (Java) / No (C#)No

La filosofia de Go es clara: los errores son valores, no excepciones. Usa panic solo para situaciones verdaderamente excepcionales.

Resumen