Manejo de errores en Go: filosofía y práctica
A diferencia de muchos lenguajes modernos, Go (Golang) no utiliza la construcción clásica try/catch. En su lugar, los desarrolladores de Go siguen el enfoque de «errores como valores» (errors as values). Esto hace que el código sea más explícito, predecible y seguro. En este artículo analizaremos todos los aspectos del manejo de errores en Go: desde patrones básicos hasta técnicas avanzadas con defer, panic y recover.
1. Fundamentos: los errores son simplemente valores
En Go, un error es una interfaz error con un único método Error() string. Cualquier función que pueda fallar devuelve un error como último valor de retorno.
package main
import ( "errors" "fmt")
// Función de división con retorno de errorfunc 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)}Punto clave: la comprobación if err != nil es la forma estándar de manejar errores en Go. No hay excepciones ocultas que puedan «saltar» inesperadamente.
2. Creación y personalización de errores
Go proporciona varias formas de crear errores:
errors.New()— error de texto simple.fmt.Errorf()— error con formato (soporta%wpara wrapping).- Tipos personalizados — para transmitir contexto adicional.
Error personalizado con datos adicionales
type DivisionError struct { Dividend float64 Divisor float64 Message string}
func (e *DivisionError) Error() string { return fmt.Sprintf("%s: %f / %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: "división por cero", } } return a / b, nil}
func main() { _, err := safeDivide(10, 0) if err != nil { // Type assertion para obtener campos adicionales var divErr *DivisionError if errors.As(err, &divErr) { fmt.Printf("Error con %f sobre %f: %s", divErr.Dividend, divErr.Divisor, divErr.Message) } }}El uso de errors.As() permite verificar si un error es de un tipo específico y extraer datos del mismo. Es el equivalente de instanceof en otros lenguajes.
3. Patrones de manejo de errores: de lo simple a lo complejo
En Go existen varios patrones establecidos para trabajar con errores:
3.1. Sentinel errors (errores centinela)
Son errores predefinidos que se pueden comparar con ==. Ejemplo: io.EOF.
import ( "errors" "fmt")
var ErrNotFound = errors.New("elemento no encontrado")
func findUser(id int) (string, error) { if id != 1 { return "", ErrNotFound } return "Alice", nil}
func main() { _, err := findUser(2) if errors.Is(err, ErrNotFound) { fmt.Println("Usuario no encontrado, creando uno nuevo...") }}3.2. Wrapping de errores (envoltura)
A partir de Go 1.13, se pueden envolver errores usando %w en fmt.Errorf. Esto preserva la cadena de errores.
func readConfig(path string) error { data, err := os.ReadFile(path) if err != nil { return fmt.Errorf("lectura del config %s: %w", path, err) } // ... procesamiento de data