Manejo de Errores en Go: Errores como Valores, no Excepciones
En los posts anteriores exploramos structs, interfaces, métodos y collections en Go. Hoy abordaremos uno de los aspectos más distintivos del lenguaje: el manejo de errores. Si vienes de Java, donde usas try-catch y excepciones, Go tiene un enfoque completamente diferente que al principio puede parecer verboso, pero que en realidad te da más control y claridad.
Filosofía: Errores como Valores
En Go, los errores son valores, no excepciones. No hay try-catch. En su lugar, las funciones retornan errores como parte de su valor de retorno:
// Convención: error siempre es el último valor de retorno
result, err := doSomething()
if err != nil {
// Manejar el error
return err
}
// Continuar con el resultado
Esta filosofía tiene ventajas importantes:
Explícito: Ves claramente qué funciones pueden fallar
Control: Decides cómo manejar cada error
Performance: No hay overhead de stack unwinding como en excepciones
Claridad: El flujo de código es más predecible
Crear Errores Básicos
Errores Simples con errors.New()
var (
ErrNotFound = errors.New("resource not found")
ErrInvalidInput = errors.New("invalid input")
ErrUnauthorized = errors.New("unauthorized")
)
Estos son sentinel errors - errores predefinidos que puedes comparar directamente. Son útiles para errores conocidos que el llamador puede manejar específicamente.
Errores con Formato con fmt.Errorf()
func createFormattedError(id int) error {
return fmt.Errorf("user with id %d not found", id)
}
fmt.Errorf() es útil cuando necesitas incluir información dinámica en el mensaje de error.
Errores Personalizados
Puedes crear tipos de error personalizados para incluir más información:
type ValidationError struct {
Field string
Message string
}
// Implementar la interfaz error
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error on field '%s': %s", e.Field, e.Message)
}
Esto te permite:
Agregar campos adicionales al error
Extraer información específica más tarde
Crear jerarquías de errores
Wrapping Errors: Agregar Contexto
Una de las características más poderosas de Go (desde Go 1.13) es el wrapping de errores. Te permite agregar contexto a un error sin perder el error original.
El Problema sin Wrapping
// ❌ Sin contexto
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err // Solo retorna el error original sin contexto
}
// ...
}
Si este error ocurre en una función profunda, no sabes dónde ni por qué falló.
La Solución: Wrapping con %w
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
// Wrap el error agregando contexto
return fmt.Errorf("failed to open file %s: %w", filename, err)
}
defer file.Close()
data := make([]byte, 100)
_, err = file.Read(data)
if err != nil && err != io.EOF {
// Wrap el error agregando contexto
return fmt.Errorf("failed to read file %s: %w", filename, err)
}
return nil
}
El verbo %w en fmt.Errorf() crea un error que envuelve el error original. Esto te permite:
Agregar contexto útil en cada capa
Mantener el error original para verificación
Crear cadenas de errores informativas
Verificar Errores Específicos: errors.Is()
errors.Is() verifica si un error es o contiene un error específico en su cadena de wrapping:
err := readFile("nonexistent.txt")
// Verificar si el error es específicamente os.ErrNotExist
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File does not exist")
}
// Verificar con nuestro error personalizado
if errors.Is(err, ErrNotFound) {
fmt.Println("Resource not found")
}
Importante: errors.Is() funciona incluso si el error está envuelto:
// Esto funciona aunque ErrNotFound esté envuelto
if errors.Is(err, ErrNotFound) {
// Manejar error específico
}
Ejemplo Práctico: Manejo de Errores Específicos
func (r *UserRepository) GetUser(id int) (*User, error) {
user, exists := r.users[id]
if !exists {
return nil, fmt.Errorf("failed to get user: %w", ErrUserNotFound)
}
return user, nil
}
// En el llamador
user, err := repo.GetUser(123)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// Manejar específicamente el caso de usuario no encontrado
return fmt.Errorf("user %d not found", 123)
}
// Otros errores
return err
}
Extraer Errores por Tipo: errors.As()
errors.As() es similar a instanceof en Java, pero para errores. Te permite extraer un error de un tipo específico de la cadena de errores:
err := &ValidationError{
Field: "email",
Message: "invalid format",
}
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("Validation error on field: %s\n", validationErr.Field)
fmt.Printf("Message: %s\n", validationErr.Message)
}
Nota: El segundo parámetro debe ser un puntero a un puntero del tipo de error.
Cuándo Usar errors.Is() vs errors.As()
| Situación | Usar |
| Comparar con error predefinido (sentinel) | errors.Is() |
| Extraer información de error personalizado | errors.As() |
| Verificar tipo de error | errors.As() |
Combinar Múltiples Errores: errors.Join()
errors.Join() (desde Go 1.20) combina múltiples errores en uno solo. Es perfecto para validaciones que pueden tener múltiples errores:
func validateUser(name, email string) error {
var errs []error
if name == "" {
errs = append(errs, &ValidationError{
Field: "name",
Message: "cannot be empty",
})
}
if email == "" {
errs = append(errs, &ValidationError{
Field: "email",
Message: "cannot be empty",
})
}
if len(email) > 0 && !strings.Contains(email, "@") {
errs = append(errs, &ValidationError{
Field: "email",
Message: "invalid format",
})
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
Para extraer los errores individuales:
if err := validateUser("", ""); err != nil {
if joinedErr, ok := err.(interface{ Unwrap() []error }); ok {
for _, e := range joinedErr.Unwrap() {
fmt.Printf(" - %v\n", e)
}
}
}
Patrón: Sentinel Errors
Los sentinel errors son errores predefinidos que representan condiciones específicas y conocidas:
var (
ErrUserNotFound = errors.New("user not found")
ErrInvalidPassword = errors.New("invalid password")
ErrUserExists = errors.New("user already exists")
)
Ventajas:
Comparación directa con
errors.Is()Documentación clara de errores posibles
Manejo específico por tipo de error
Ejemplo de uso:
func (r *UserRepository) CreateUser(id int, username, email string) error {
if _, exists := r.users[id]; exists {
return ErrUserExists // Retornar sentinel error
}
r.users[id] = &User{ID: id, Username: username, Email: email}
return nil
}
// En el llamador
if err := repo.CreateUser(1, "john", "john@example.com"); err != nil {
if errors.Is(err, ErrUserExists) {
// Manejar específicamente el caso de usuario existente
fmt.Println("User already exists, continuing...")
} else {
return fmt.Errorf("failed to create user: %w", err)
}
}
Manejo de Errores en Capas
En aplicaciones reales, los errores pasan por múltiples capas. Es importante agregar contexto en cada capa mientras preservas el error original:
// Capa de repositorio
func (r *UserRepository) GetUser(id int) (*User, error) {
user, exists := r.users[id]
if !exists {
return nil, fmt.Errorf("failed to get user: %w", ErrUserNotFound)
}
return user, nil
}
// Capa de servicio
func (s *UserService) Login(username, password string) (*User, error) {
user, err := s.repo.Authenticate(username, password)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
return nil, fmt.Errorf("login failed: user not found")
}
if errors.Is(err, ErrInvalidPassword) {
return nil, fmt.Errorf("login failed: invalid password")
}
return nil, fmt.Errorf("login failed: %w", err)
}
return user, nil
}
Principios:
Wrap en cada capa con contexto relevante
Verifica errores específicos con
errors.Is()cuando sea apropiadoNo ignores errores - siempre maneja o propaga
Agrega contexto útil - qué operación falló y dónde
Comparación: Java vs Go
| Aspecto | Java | Go |
| Mecanismo | Excepciones (throw/catch) | Valores de retorno |
| Visibilidad | Implícita (cualquier función puede lanzar) | Explícita (error en firma) |
| Performance | Overhead de stack unwinding | Sin overhead |
| Control de flujo | Puede saltar múltiples niveles | Flujo explícito |
| Contexto | Stack trace automático | Debes agregarlo manualmente |
| Verificación | instanceof | errors.Is() / errors.As() |
Ejemplo Comparativo
Java:
try {
User user = repository.getUser(id);
// ...
} catch (UserNotFoundException e) {
// Manejar error
} catch (Exception e) {
// Manejar otros errores
}
Go:
user, err := repository.GetUser(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// Manejar error específico
return fmt.Errorf("user %d not found", id)
}
// Manejar otros errores
return fmt.Errorf("failed to get user: %w", err)
}
// Continuar con user
Mejores Prácticas
1. Siempre Verifica Errores
// ❌ MAL - Ignorar error
result, _ := doSomething()
// ✅ BIEN - Verificar error
result, err := doSomething()
if err != nil {
return err
}
2. Agrega Contexto con Wrapping
// ❌ MAL - Sin contexto
if err != nil {
return err
}
// ✅ BIEN - Con contexto
if err != nil {
return fmt.Errorf("failed to process user %d: %w", userID, err)
}
3. Usa Sentinel Errors para Errores Conocidos
// ✅ Definir errores predefinidos
var ErrUserNotFound = errors.New("user not found")
// ✅ Retornar sentinel error
if !exists {
return ErrUserNotFound
}
// ✅ Verificar con errors.Is()
if errors.Is(err, ErrUserNotFound) {
// Manejar específicamente
}
4. No Envuelvas Errores Múltiples Veces Sin Razón
// ❌ MAL - Envolver sin agregar valor
if err != nil {
return fmt.Errorf("error: %w", err) // No agrega contexto útil
}
// ✅ BIEN - Agregar contexto útil
if err != nil {
return fmt.Errorf("failed to save user %s: %w", username, err)
}
5. Usa errors.Join() para Múltiples Errores
// ✅ Para validaciones con múltiples errores
var errs []error
if name == "" {
errs = append(errs, ErrNameRequired)
}
if email == "" {
errs = append(errs, ErrEmailRequired)
}
if len(errs) > 0 {
return errors.Join(errs...)
}
Errores Comunes
❌ Error 1: Ignorar Errores
// ❌ MAL
result, _ := doSomething() // Ignora el error
// ✅ BIEN
result, err := doSomething()
if err != nil {
return err
}
❌ Error 2: No Agregar Contexto
// ❌ MAL
if err != nil {
return err // Sin contexto
}
// ✅ BIEN
if err != nil {
return fmt.Errorf("failed to process: %w", err)
}
❌ Error 3: Comparar Errores Envueltos con ==
// ❌ MAL - No funciona con errores envueltos
if err == ErrNotFound {
// ...
}
// ✅ BIEN - Usa errors.Is()
if errors.Is(err, ErrNotFound) {
// ...
}
Ejemplo Completo: Sistema de Autenticación
type UserService struct {
repo *UserRepository
}
func (s *UserService) RegisterUser(id int, username, email, password string) error {
// Validar entrada
if err := validateUser(username, email); err != nil {
return fmt.Errorf("validation failed: %w", err)
}
// Crear usuario
if err := s.repo.CreateUser(id, username, email); err != nil {
if errors.Is(err, ErrUserExists) {
return fmt.Errorf("registration failed: user already exists")
}
return fmt.Errorf("registration failed: %w", err)
}
return nil
}
func (s *UserService) Login(username, password string) (*User, error) {
user, err := s.repo.Authenticate(username, password)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
return nil, fmt.Errorf("login failed: user not found")
}
if errors.Is(err, ErrInvalidPassword) {
return nil, fmt.Errorf("login failed: invalid password")
}
return nil, fmt.Errorf("login failed: %w", err)
}
return user, nil
}
Este ejemplo muestra:
Validación con
errors.Join()Manejo específico de sentinel errors
Agregar contexto en cada capa
Preservar el error original para verificación
Conclusiones
El manejo de errores en Go es diferente pero poderoso:
✅ Errores como valores: Más explícito y controlable que excepciones
✅ Wrapping: Agrega contexto sin perder el error original
✅ errors.Is(): Verifica errores específicos incluso cuando están envueltos
✅ errors.As(): Extrae información de errores personalizados
✅ errors.Join(): Combina múltiples errores para validaciones
Conceptos clave a recordar:
Siempre verifica errores explícitamente
Agrega contexto con
fmt.Errorf()y%wUsa sentinel errors para errores conocidos
Verifica con
errors.Is(), no con==Extrae información con
errors.As()para errores personalizados
Aunque puede parecer verboso al principio, este enfoque te da más control y hace que el código sea más predecible y fácil de depurar.
Próximos Pasos
En el siguiente post exploraremos los paquetes importantes de Go: context, io, fmt, sync, y reflect. Estos paquetes son fundamentales para escribir código Go profesional y entenderás cómo se integran con todo lo que hemos aprendido hasta ahora.
¿Tienes preguntas sobre el manejo de errores en Go? Déjame saber en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio go-mastery-lab.
Enjoy!
José Díaz
+51 939965148




