En esta página
Manejo de errores
Manejo de errores en Go: explícito y robusto
El manejo de errores es uno de los aspectos más debatidos y a la vez más poderosos de Go. Go no tiene excepciones. En lugar de try/catch, los errores son simplemente valores que las funciones retornan. Esto obliga a que el manejo de errores sea explícito y visible en el código.
La crítica más común de Go es "hay demasiado if err != nil". La respuesta de la comunidad Go es que esta visibilidad es una característica, no un bug: nunca puedes ignorar accidentalmente un error. En Java, puedes olvidar un bloque catch y el error se propaga silenciosamente. En Go, el compilador te obliga a manejar cada error.
La interfaz `error`
En Go, error es simplemente una interfaz de la biblioteca estándar:
type error interface {
Error() string
}Cualquier tipo que tenga un método Error() string es un error. Esto hace que crear errores personalizados sea sencillo.
Creando errores simples
import "errors"
// errors.New — el más simple
var ErrSimple = errors.New("algo salió mal")
// fmt.Errorf — con formato
func validarEdad(edad int) error {
if edad < 0 {
return fmt.Errorf("edad inválida: %d (debe ser >= 0)", edad)
}
if edad > 150 {
return fmt.Errorf("edad inválida: %d (máximo 150)", edad)
}
return nil
}El patrón idiomático de manejo de errores
// Función que puede fallar retorna (resultado, error)
func leerArchivo(ruta string) ([]byte, error) {
f, err := os.Open(ruta)
if err != nil {
return nil, fmt.Errorf("leerArchivo: %w", err)
}
defer f.Close()
datos, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("leerArchivo — lectura: %w", err)
}
return datos, nil
}
// Caller: siempre verificar el error antes de usar el resultado
datos, err := leerArchivo("config.json")
if err != nil {
log.Fatal("no se pudo leer la configuración:", err)
}
// Solo aquí es seguro usar datos
procesarDatos(datos)Errores centinela: valores predefinidos comparables
Un error centinela es una variable de error predefinida a nivel de paquete, comparable con == o errors.Is:
// Definición de errores centinela
var (
ErrNoEncontrado = errors.New("no encontrado")
ErrSinAutorización = errors.New("sin autorización")
ErrConflicto = errors.New("conflicto de recursos")
ErrInválido = errors.New("parámetro inválido")
)
// Uso
func obtenerProducto(id int) (*Producto, error) {
if id <= 0 {
return nil, fmt.Errorf("obtenerProducto: %w", ErrInválido)
}
p, existe := db[id]
if !existe {
return nil, fmt.Errorf("obtenerProducto id=%d: %w", id, ErrNoEncontrado)
}
return p, nil
}
// Comparar con errors.Is (atraviesa el wrapping)
p, err := obtenerProducto(0)
if errors.Is(err, ErrInválido) {
fmt.Println("ID inválido:", err)
} else if errors.Is(err, ErrNoEncontrado) {
fmt.Println("Producto no existe:", err)
}Los errores centinela son convención en la biblioteca estándar: io.EOF, sql.ErrNoRows, os.ErrNotExist, etc.
Errores personalizados con datos
Cuando necesitas llevar información adicional en el error, crea un tipo personalizado:
// Error con código HTTP
type ErrorHTTP struct {
Código int
Mensaje string
Detalle string
}
func (e *ErrorHTTP) Error() string {
return fmt.Sprintf("HTTP %d: %s — %s", e.Código, e.Mensaje, e.Detalle)
}
// Error de validación con múltiples campos
type ErroresValidación struct {
Campos map[string][]string
}
func (e *ErroresValidación) Error() string {
return fmt.Sprintf("validación falló: %d campo(s) con errores", len(e.Campos))
}
func (e *ErroresValidación) Agregar(campo, mensaje string) {
if e.Campos == nil {
e.Campos = make(map[string][]string)
}
e.Campos[campo] = append(e.Campos[campo], mensaje)
}
func (e *ErroresValidación) TieneErrores() bool {
return len(e.Campos) > 0
}`fmt.Errorf` con `%w`: wrapping de errores
El operador %w en fmt.Errorf crea un error que envuelve al original, preservando la cadena de errores:
func operaciónA() error {
return errors.New("error de base de datos")
}
func operaciónB() error {
if err := operaciónA(); err != nil {
return fmt.Errorf("operaciónB falló: %w", err) // envuelve el error
}
return nil
}
func operaciónC() error {
if err := operaciónB(); err != nil {
return fmt.Errorf("operaciónC falló: %w", err) // envuelve de nuevo
}
return nil
}
// La cadena completa de errores
err := operaciónC()
fmt.Println(err)
// "operaciónC falló: operaciónB falló: error de base de datos"`errors.Is`: verificar errores en la cadena
errors.Is recorre la cadena de wrapping buscando un error específico:
var ErrOriginal = errors.New("error original")
err := fmt.Errorf("contexto: %w", fmt.Errorf("más contexto: %w", ErrOriginal))
// errors.Is atraviesa la cadena
fmt.Println(errors.Is(err, ErrOriginal)) // true
// == solo compara el error superficial
fmt.Println(err == ErrOriginal) // false`errors.As`: extraer el tipo concreto de la cadena
errors.As recorre la cadena de wrapping buscando un error de un tipo específico:
type ErrorBD struct {
Query string
Causa error
}
func (e *ErrorBD) Error() string {
return fmt.Sprintf("query %q falló: %v", e.Query, e.Causa)
}
func (e *ErrorBD) Unwrap() error {
return e.Causa
}
// En alguna función profunda
err := fmt.Errorf("capa servicio: %w", &ErrorBD{
Query: "SELECT * FROM usuarios",
Causa: errors.New("timeout"),
})
// Extraer el ErrorBD desde cualquier punto de la cadena
var errBD *ErrorBD
if errors.As(err, &errBD) {
fmt.Printf("Query que falló: %s\n", errBD.Query)
}Implementar `Unwrap()` para errores personalizados
Para que tu error funcione con errors.Is y errors.As, implementa Unwrap():
type ErrorContexto struct {
Mensaje string
Causa error
}
func (e *ErrorContexto) Error() string {
return fmt.Sprintf("%s: %v", e.Mensaje, e.Causa)
}
// Unwrap permite que errors.Is y errors.As traversen la cadena
func (e *ErrorContexto) Unwrap() error {
return e.Causa
}`panic` y `recover`: el último recurso
panic es para situaciones verdaderamente excepcionales — violaciones de invariantes del programa, condiciones que nunca deberían ocurrir en producción:
func dividir(a, b int) int {
if b == 0 {
panic("división por cero: invariante violado")
}
return a / b
}
// recover captura un panic — solo útil en defer
func manejarPanic() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Panic recuperado:", r)
}
}()
panic("algo muy malo")
// Nunca se ejecuta:
fmt.Println("esto no se imprime")
}El uso legítimo más común de recover es en los servidores HTTP para evitar que un pánico en un handler mate todo el servidor. La biblioteca estándar de Go hace esto internamente.
Errores en librerías vs. aplicaciones
- Librerías: retorna errores ricos, nunca llames
log.Fatalnios.Exit - Aplicaciones: puedes usar
log.Fatal(err)cuando el error es irrecuperable al inicio
// En una librería
func Conectar(dsn string) (*BD, error) {
// ... retorna error al caller para que decida qué hacer
}
// En main de una aplicación
bd, err := milib.Conectar(dsn)
if err != nil {
log.Fatalf("no se pudo conectar a la base de datos: %v", err)
}Con el manejo de errores dominado, en la siguiente lección entraremos al corazón de Go: las goroutines — la forma en que Go hace que la concurrencia sea accesible y eficiente.
Inicia sesión para guardar tu progreso