`defer`, `panic` y `recover`
Go no tiene `try/catch`. En su lugar tienes `defer` para limpieza garantizada y, en casos excepcionales, `panic` + `recover` para abortar y reanudar.
defer: ejecuta al salir de la función
defer fn() pospone la ejecución de fn() hasta que la función que lo contiene retorne — sea por return, por llegar al final o por un panic. Es la forma idiomática de garantizar limpieza.
package main
import "fmt"
func main() {
fmt.Println("inicio")
defer fmt.Println("limpieza al final")
fmt.Println("trabajo")
// "limpieza al final" se imprime justo antes de salir de main
}
inicio
trabajo
limpieza al finalOrden LIFO: varios defer
Los defer se apilan: el último en declararse es el primero en ejecutarse. Piensa en una pila de funciones que se desapilan al retornar.
package main
import "fmt"
func main() {
defer fmt.Println("1: primero declarado")
defer fmt.Println("2: segundo declarado")
defer fmt.Println("3: tercero declarado")
fmt.Println("cuerpo de main")
}
cuerpo de main
3: tercero declarado
2: segundo declarado
1: primero declaradoCasos de uso típicos: archivos y locks
defer brilla emparejado con cualquier recurso que debe liberarse: archivos, mutexes, conexiones, transacciones. Pones la apertura y el defer de cierre juntos — imposible olvidar cerrar.
package main
import (
"fmt"
"os"
"sync"
)
var mu sync.Mutex
var counter int
func increment() {
mu.Lock()
defer mu.Unlock() // se libera SÍ o SÍ al retornar
counter++
}
func readConfig(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // siempre cierra, incluso si más abajo hay un return
// ...leer el archivo, parsear, etc...
_ = f
return nil
}
func main() {
increment()
increment()
increment()
fmt.Println("counter:", counter)
if err := readConfig("nope.conf"); err != nil {
fmt.Println("config error:", err)
}
}
counter: 3
config error: open nope.conf: no such file or directoryLos argumentos se evalúan al declarar el defer
Cuando escribes defer fn(x), el valor de x se captura en ese instante — no en el momento de ejecución. Esto es una fuente común de confusión.
package main
import "fmt"
func main() {
x := 10
// El valor 10 se captura ya
defer fmt.Println("defer con valor capturado:", x)
x = 99
// Si quieres ver el valor final, usa una closure
defer func() {
fmt.Println("defer con closure:", x)
}()
x = 777
fmt.Println("antes de retornar, x =", x)
}
antes de retornar, x = 777
defer con closure: 777
defer con valor capturado: 10Si pasas el valor como argumento al defer, se congela. Si lo lees dentro de una closure (defer func() { ... }()), verás el valor que tenga al ejecutarse.
panic: aborto controlado
panic detiene el flujo normal: la función actual deja de ejecutar su cuerpo, pero sus defer sí corren, y el panic se propaga hacia arriba por la pila de llamadas hasta hacer crashear el programa.
package main
import "fmt"
func divide(a, b int) int {
if b == 0 {
panic("división por cero")
}
return a / b
}
func main() {
defer fmt.Println("este defer sí se ejecuta")
fmt.Println(divide(10, 2)) // 5
fmt.Println(divide(10, 0)) // panic aquí
fmt.Println("nunca llega")
}
5
este defer sí se ejecuta
panic: división por cerorecover: capturar un panic dentro de un defer
recover() solo funciona dentro de un defer. Si hay un panic activo, lo captura, detiene su propagación y devuelve el valor con el que se llamó al panic. Si no hay panic, devuelve nil.
package main
import "fmt"
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
// convertimos el panic en un error
err = fmt.Errorf("recovered: %v", r)
}
}()
return a / b, nil
}
func main() {
r, err := safeDivide(10, 2)
fmt.Println(r, err) // 5 <nil>
r, err = safeDivide(10, 0)
fmt.Println(r, err) // 0 recovered: runtime error: integer divide by zero
fmt.Println("el programa sigue vivo")
}
5 <nil>
0 recovered: runtime error: integer divide by zero
el programa sigue vivoCuándo NO usar panic
El idiomático de Go es devolver error, no entrar en pánico. panic se reserva para situaciones realmente excepcionales: estado interno corrupto, invariantes rotas, errores de programación.
package main
import (
"errors"
"fmt"
)
// ❌ Mal: panic por una entrada inválida normal
func parseAgeBad(s string) int {
if s == "" {
panic("empty string") // no es excepcional, es input del usuario
}
return 0
}
// ✅ Bien: devuelve un error
func parseAgeGood(s string) (int, error) {
if s == "" {
return 0, errors.New("empty string")
}
return 0, nil
}
func main() {
if _, err := parseAgeGood(""); err != nil {
fmt.Println("error esperado:", err)
}
}
error esperado: empty stringSi el llamador podría manejar el problema, devuelve error. Reserva panic para fallos que indican un bug en tu código (slice fuera de rango, invariante violada, mapa nil al escribir). Y usa recover solo en fronteras claras — un servidor HTTP, un worker en goroutine — no como try/catch general.