Manejo de errores en Go: filosofía sin try/catch
En el mundo de Go (Golang) no existe el mecanismo try/catch familiar para muchos desarrolladores. Esto no es una casualidad, sino una decisión arquitectónica consciente. Los creadores del lenguaje consideraron que el manejo explícito de errores hace que el código sea más confiable y legible. En lugar de ocultar excepciones en la pila de llamadas, Go le propone al programador verificar siempre el resultado de cada operación potencialmente peligrosa. En este artículo analizaremos todos los enfoques principales para el manejo de errores en Go: desde los error básicos hasta los potentes panic y recover, y también mostraremos cómo escribir código que no falle en el momento más inoportuno.
1. Mecanismo básico: interfaz error
La base del manejo de errores en Go es la interfaz incorporada error. Tiene solo un método:
type error interface { Error() string}Cualquier función que pueda fallar debe devolver error como último valor. Se acostumbra devolver nil si no hay error, y un valor no nulo si ocurrió una falla.
Ejemplo de manejo básico:
package main
import ( "errors" "fmt")
func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("división por cero no es posible") } return a / b, nil}
func main() { result, err := divide(10, 0) if err != nil { fmt.Println("Error:", err) return } fmt.Println("Resultado:", result)}Este es el equivalente de try/catch en Go: la verificación explícita if err != nil. Este enfoque obliga al desarrollador a pensar en los errores en cada paso.
2. Tipos de error personalizados y wrapping
A menudo no es suficiente simplemente devolver una cadena con la descripción. Es necesario transmitir contexto: dónde ocurrió el error, qué parámetros se pasaron. Para esto se crean tipos de error propios.
Creación de un error personalizado:
type DivisionError struct { Dividend float64 Divisor float64 Message string}
func (e *DivisionError) Error() string { return fmt.Sprintf("error de división: %s (dividendo: %f, divisor: %f)", e.Message, e.Dividend, e.Divisor)}
func safeDivide(a, b float64) (float64, error) { if b == 0 { return 0, &DivisionError{Dividend: a, Divisor: b, Message: "el divisor es cero"} } return a / b, nil}Wrapping de errores:
A partir de Go 1.13, surgió la posibilidad de envolver un error dentro de otro, agregando contexto:
import "fmt"
func readConfig(path string) error { file, err := openFile(path) if err != nil { return fmt.Errorf("no se pudo leer la configuración %s: %w", path, err) } defer file.Close() // ... lectura del archivo return nil}La directiva %w crea una cadena de errores. Posteriormente puede verificar si el error es de un tipo específico usando errors.Is() y errors.As().
3. Panic y Recover: situaciones de emergencia
A veces ocurre algo que no debería suceder en principio: salirse de los límites de un array, desreferenciar un puntero nulo. En tales casos, Go usa panic. Esto no es un reemplazo de try/catch, sino un mecanismo para errores críticos e irrecuperables.
Ejemplo de panic:
func main() { arr := []int{1, 2, 3} fmt.Println(arr[10]) // panic: runtime error: index out of range}Sin embargo, si desea "atrapar" el pánico y restaurar la ejecución, se usa recover junto con defer. Este es el análogo más cercano a catch.
Ejemplo de recover:
func safeCall(f func()) { defer func() { if r := recover(); r != nil { fmt.Pr