Panic, Recover y Defer Avanzado
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
| Escenario | Ejemplo |
|---|---|
| Errores de programador | Indice fuera de rango, nil pointer |
| Estado imposible | Un switch que cubre todos los casos |
| Inicializacion fallida | No se puede conectar a un recurso critico al arrancar |
| Violacion de invariantes | Precondiciones 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
- Errores esperados: archivos inexistentes, input de usuario invalido, timeouts de red
- Flujo normal de errores: usa el patron
(valor, error)idiomatico de Go - Control de flujo: panic no es un substituto de excepciones
// 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
| Aspecto | Go (panic/recover) | Java/C# (try/catch) | Python (try/except) |
|---|---|---|---|
| Uso principal | Errores irrecuperables | Cualquier error | Cualquier error |
| Flujo normal de errores | Return error | Throw exceptions | Raise exceptions |
| Costo de rendimiento | Minimo (raro) | Overhead en throw | Overhead en raise |
| Filosofia | Explicito, valores de retorno | Excepciones como flujo | Excepciones como flujo |
| Propagacion | Manual (return err) | Automatica (stack unwinding) | Automatica |
| Checked exceptions | No existen | Si (Java) / No (C#) | No |
La filosofia de Go es clara: los errores son valores, no excepciones. Usa panic solo para situaciones verdaderamente excepcionales.
Resumen
panicdetiene la ejecucion normal; usalo solo para errores de programador o estados imposiblesdeferejecuta en orden LIFO y evalua argumentos inmediatamente- Evita
deferen loops; extrae a funciones independientes recoversolo funciona dentro dedefery captura el valor del panic- El patron
safeExecuteconvierte panics en errores manejables - Go prefiere errores como valores de retorno sobre excepciones