Si en tus dashboards de Datadog o ELK ves el mismo error de base de datos repetido cinco veces en una sola traza, tienes un problema de diseño. La regla es absoluta: una función o añade contexto al error usando fmt.Errorf con el verbo %w [disponible desde Go 1.13] para permitir el error unwrapping, o lo loguea y deja de propagarlo. Nunca ambas.
Este patrón se basa en la jerarquía de responsabilidad: cada capa de tu aplicación tiene una responsabilidad distinta respecto al error. Si una función de la capa de dominio detecta un fallo, su trabajo es explicar qué estaba intentando hacer cuando falló, no escribir en la salida estándar. El “dueño” del error es la capa superior que no puede hacer nada más con él (generalmente el Handler de un servidor HTTP o un comando de CLI) y es la única que tiene el contexto suficiente (como un request_id o un trace_id) para emitir un log definitivo. Si fallas en esto, generas “log spam”, lo que aumenta los costes de ingesta de telemetría y oscurece la causa raíz al inundar el flujo de logs con ruido redundante.
package main
import (
"errors"
"fmt"
"log"
"net/http"
)
// Error personalizado para la capa de base de datos
var errDBConnection = errors.New("dial tcp 127.0.0.1:5432: i/o timeout")
// Repository simula la capa de infraestructura
type Repository struct{}
func (r *Repository) GetUser(id string) error {
// Simulamos un fallo de conexión.
// No logueamos aquí; solo retornamos el error crudo.
return errDBConnection
}
// Service simula la capa de lógica de negocio (dominio)
type Service struct {
repo *Repository
}
func (s *Service) FetchUserConfig(id string) error {
err := s.repo.GetUser(id)
if err != nil {
// Enriquecemos el error con contexto de la operación de negocio.
// Usamos %w para que el caller pueda inspeccionar el error original.
return fmt.Errorf("service: failed to fetch user config for %s: %w", id, err)
}
return nil
}
// Handler simula la capa de transporte (HTTP)
type UserHandler struct {
svc *Service
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("id")
// La capa de transporte es el punto final de la ejecución de la request.
// Es la única capa autorizada para decidir si el error se loguea o se ignora.
if err := h.svc.FetchUserConfig(userID); err != nil {
// Logueamos el error completo una sola vez con el contexto de la request.
log.Printf("[ERROR] request_id=%s method=%s: %v", r.Header.Get("X-Request-ID"), r.Method, err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "Internal Server Error")
return
}
w.WriteHeader(http.StatusOK)
}
func main() {
// Configuración manual para el ejemplo
repo := &Repository{}
svc := &Service{repo: repo}
handler := &UserHandler{svc: svc}
// Simulamos una petición HTTP
req, _ := http.NewRequest("GET", "/user?id=123", nil)
req.Header.Set("X-Request-ID", "req-abc-123")
// En un servidor real, esto sería controlado por http.ListenAndServe
handler.ServeHTTP(nil, req)
}
En el ejemplo anterior, observa cómo fluye la información. Cuando repo.GetUser falla, devuelve errDBConnection. La función s.FetchUserConfig recibe ese error y, en lugar de imprimirlo, lo envuelve con fmt.Errorf y el verbo %w. Esto crea una cadena de errores donde la información de la causa raíz está preservada pero ahora incluye el contexto de “qué” estaba intentando hacer el servicio (failed to fetch user config).
Finalmente, llegamos a h.ServeHTTP. Como el Handler es el punto de entrada que gestiona la comunicación con el cliente externo, es el único con la autoridad para decidir que este error es digno de ser registrado en el sistema de logs. Al llamar a log.Printf, el mensaje resultante es una línea limpia y completa: [ERROR] request_id=req-abc-123 method=GET: service: failed to fetch user config for 123: dial tcp 127.0.0.1:5432: i/o timeout. Tenemos el ID de la petición, la acción y la causa raíz, todo en una sola entrada de log.
El error frecuente ocurre cuando los desarrolladores, por miedo a perder la trazabilidad, implementan este patrón:
func (s *Service) DoSomething() error {
err := s.repo.Action()
if err != nil {
// ANTI-PATRÓN: Loguear y retornar
log.Printf("error en service: %v", err)
return err
}
return nil
}
Si tienes una arquitectura de 5 capas y todas siguen este anti-patrón, una única falla en la base de datos generará 5 líneas de log casi idénticas. Esto no solo ensucia la consola, sino que en sistemas distribuidos con millones de eventos por segundo, puede disparar falsos positivos en los sistemas de alerta y disparar los costes de almacenamiento de logs innecesariamente.
N° 211