En Go, los errores no son excepciones que interrumpen el flujo, sino valores que se devuelven para ser tratados explícitamente. Para gestionar estos valores de forma profesional, existen tres patrones principales: los errores centinela (sentinel), los tipos de error personalizados (custom error types) y los errores opacos.
Un error sentinela es una variable de paquete predefinida que representa un estado específico, como “no encontrado”. Se utiliza cuando el valor del error es suficiente para la lógica de control y no necesitas datos adicionales. Por otro lado, los tipos de error personalizados son estructuras (structs) que implementan la interfaz error. Estos se usan cuando necesitas transportar metadatos (como un campo de validación o un código de estado HTTP) junto con el mensaje. Finalmente, los errores opacos son aquellos que encapsulan su implementación interna; el llamador recibe un error, pero la estructura específica queda oculta, permitiendo que el programador solo acceda al mensaje de texto, manteniendo la integridad de la abstracción.
Si eliges mal el patrón, podrías romper la capacidad de tu código para escalar o para ser testeado. Usar sentinelas para errores complejos que requieren contexto hará que tu código sea imposible de depurar en producción. Usar tipos personalizados sin la debida encapsulación puede acoplar innecesariamente a tus usuarios a tus estructuras internas. Y si intentas inspeccionar un error mediante una comparación de strings (err.Error() == "...") en lugar de usar las herramientas adecuadas, romperás tu lógica en cuanto alguien decida envolver (wrap) el error para añadir contexto.
package main
import (
"errors"
"fmt"
)
// 1. Errores Sentinel: Variables globales para estados conocidos.
// Se usan con errors.Is.
var ErrUserNotFound = errors.New("usuario no encontrado")
// 2. Custom Error Type: Structs que cargan contexto extra.
// Se usan con errors.As.
type ValidationError struct {
Field string
Issue string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validación fallida en %s: %s", e.Field, e.Issue)
}
// Repositorio simulado
type UserRepository struct{}
func (r *UserRepository) GetUser(id int) error {
if id <= 0 {
// Devolvemos un error de tipo personalizado con contexto.
return &ValidationError{
Field: "ID",
Issue: "debe ser mayor a cero",
}
}
if id > 100 {
// Devolvemos un error centinela.
return ErrUserNotFound
}
return nil
}
// Servicio que aplica el patrón de error opaco mediante el envoltorio.
func (r *UserRepository) Process(id int) error {
err := r.GetUser(id)
if err != nil {
// Al usar fmt.Errorf con %w, creamos un error que envuelve al original.
// Para el llamador, si no conoce el tipo, es un error "opaco" que
// transporta información pero mantiene la jerarquía.
return fmt.Errorf("error en el proceso de usuario: %w", err)
}
return nil
}
func main() {
repo := &UserRepository{}
// Escenario A: Error de validación (Custom Type)
errVal := repo.Process(0)
var valErr *ValidationError
if errors.As(errErr(errVal), &valErr) {
fmt.Printf("Detectado error de validación -> Campo: %s\n", valErr.Field)
}
// Escenario B: Usuario no encontrado (Sentinel)
errNotFound := repo.Process(150)
if errors.Is(errNotFound, ErrUserNotFound) {
fmt.Println("Acción: Notificar que el usuario no existe.")
}
// Escenario C: Error opaco / Desconocido
// Si recibimos un error genérico, solo podemos confiar en su mensaje.
errUnknown := fmt.Errorf("algo salió muy mal internamente")
fmt.Printf("Error genérico: %v\n", errUnknown)
}
// Helper para el ejemplo
func errErr(err error) error {
return err
}
Desglose del código
En el ejemplo, ErrUserNotFound es nuestro error centinela. Fíjate que en el main, no comparamos err == ErrUserNotFound (lo cual fallaría si el error hubiera sido envuelto con %w), sino que usamos errors.Is. Esto permite que la función errors.Is navegue por el árbol de errores hasta encontrar el valor original.
Para la validación, definimos ValidationError. Al ser un struct, podemos transportar el Field y el Issue. En el main, la función errors.As(err, &valErr) intenta extraer la estructura ValidationError del error recibido. Si tiene éxito, valErr se convierte en un puntero a la estructura real, permitiéndonos acceder a sus campos para lógica de negocio (como marcar un campo de un formulario en rojo).
La función Process implementa un patrón de error envoltorio (wrapping). Al usar fmt.Errorf("... %w", err), estamos creando una cadena de errores. Esto es vital para la observabilidad: el log mostrará "error en el proceso de usuario: validación fallida en ID: debe ser mayor a cero", dándonos el contexto de la llamada, pero permitiendo que las funciones superiores sigan detectando el error original mediante errors.Is o errors.As.
El error frecuente
Un error clásico al empezar con Go es intentar comparar errores sentinela usando el operador de igualdad == después de haberlos envuelto.
// MAL: Esto fallará si el error fue envuelto con %w
if err == ErrUserNotFound {
// ...
}
// BIEN: Esto funciona siempre, sin importar cuántas capas de wrap existan
if errors.Is(err, ErrUserNotFound) {
// ...
}
Si tu función devuelve fmt.Errorf("contexto: %w", ErrUserNotFound), el valor de err ya no es el mismo puntero que ErrUserNotFound, por lo que == devolverá false. Usa siempre errors.Is para sentinelas y errors.As para tipos personalizados.
N° 92