Middlewares en Go: logging, auth, errores y recuperación

Cómo funcionan los middlewares en Go con ejemplos de logging, autenticación, request ID, recuperación de panic y métricas.

Cover for Middlewares en Go: logging, auth, errores y recuperación

Los middlewares son el equivalente en Go a los filtros de Spring o a los middlewares de Express. Un concepto simple: una función que envuelve a otra función y ejecuta lógica antes, después o alrededor de cada petición HTTP. Cuando se usan bien, eliminan duplicación y centralizan preocupaciones transversales. Cuando se usan mal, esconden lógica de negocio en capas invisibles y convierten el debugging en una pesadilla.

En Go, un middleware no es una anotación mágica ni un framework. Es una función que recibe un handler y devuelve otro handler. Nada más. Y esa simplicidad es exactamente lo que lo hace tan potente.


Qué es un middleware en Go

Un middleware en Go es una función que envuelve un http.Handler o un http.HandlerFunc. Su trabajo es interceptar la petición antes de que llegue al handler real, ejecutar alguna lógica (logging, autenticación, métricas), y decidir si la petición continúa o se corta.

El patrón es siempre el mismo:

func MiMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Lógica antes del handler
        next.ServeHTTP(w, r)
        // Lógica después del handler
    })
}

Esto es todo. No hay interfaces que implementar, no hay clases abstractas, no hay registro en un contenedor. Una función que recibe un handler, devuelve un handler, y en medio hace lo que necesites.

La firma func(http.Handler) http.Handler es la convención estándar. Si ves esta firma en cualquier librería de Go, sabes que es un middleware compatible con net/http. Esto es importante porque significa que los middlewares son componibles: puedes encadenar diez middlewares de distintas librerías y todos funcionan juntos sin adaptadores.

Si vienes de Spring, piensa en un HandlerInterceptor pero sin @Component, sin @Order, sin XML. Si vienes de Express, es exactamente el mismo concepto pero tipado y sin next() que te puedes olvidar de llamar… bueno, en Go también te lo puedes olvidar, pero el compilador al menos te ayuda con el tipo.


Tu primer middleware: logging de duración

El middleware más útil y el primero que deberías escribir en cualquier proyecto es uno que registre cuánto tarda cada petición. Es simple, tiene un valor inmediato en producción y sirve como plantilla para entender el patrón.

package middleware

import (
    "log/slog"
    "net/http"
    "time"
)

func Logging(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()

            // Envolver el ResponseWriter para capturar el status code
            ww := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

            next.ServeHTTP(ww, r)

            logger.Info("request completed",
                slog.String("method", r.Method),
                slog.String("path", r.URL.Path),
                slog.Int("status", ww.statusCode),
                slog.Duration("duration", time.Since(start)),
            )
        })
    }
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

Hay un detalle importante aquí: el responseWriter envolvente. El http.ResponseWriter estándar no expone el status code después de que se escribe. Necesitas interceptar la llamada a WriteHeader para capturarlo. Este patrón lo vas a usar en muchos middlewares.

Fíjate también en que el middleware recibe un *slog.Logger como parámetro. Esto es inyección de dependencias al estilo Go: pasas lo que necesitas como argumento. El middleware devuelve otra función que es el middleware real. Este patrón de “factory function” es estándar cuando tu middleware necesita configuración.

Para usarlo:

func main() {
    logger := slog.Default()
    mux := http.NewServeMux()

    mux.HandleFunc("GET /health", handleHealth)
    mux.HandleFunc("GET /users/{id}", handleGetUser)

    handler := middleware.Logging(logger)(mux)
    http.ListenAndServe(":8080", handler)
}

Cada petición ahora genera un log estructurado con método, path, status code y duración. Sin tocar ni una línea de los handlers.


Middleware de Request ID

En producción, cuando tienes cientos de peticiones por segundo, necesitas poder trazar una petición específica a través de todos tus logs. El request ID es la forma estándar de hacerlo: generas un identificador único para cada petición y lo propagues a través del context.

package middleware

import (
    "context"
    "net/http"

    "github.com/google/uuid"
)

type contextKey string

const RequestIDKey contextKey = "request_id"

func RequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Si el cliente ya envía un request ID, lo reutilizamos
        id := r.Header.Get("X-Request-ID")
        if id == "" {
            id = uuid.New().String()
        }

        // Añadir al context para que lo usen otros middlewares y handlers
        ctx := context.WithValue(r.Context(), RequestIDKey, id)

        // Añadir al header de respuesta para que el cliente pueda trazarlo
        w.Header().Set("X-Request-ID", id)

        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Helper para extraer el request ID del context
func GetRequestID(ctx context.Context) string {
    if id, ok := ctx.Value(RequestIDKey).(string); ok {
        return id
    }
    return ""
}

El uso de context.WithValue es clave aquí. El context en Go es el mecanismo estándar para pasar valores a través de la cadena de handlers sin contaminar las firmas de las funciones. El request ID es uno de los casos de uso más legítimos para context.WithValue.

Un detalle que vale la pena mencionar: usamos un tipo contextKey privado como clave del context. Esto evita colisiones con otros paquetes que pudieran usar "request_id" como clave. Es una convención idiomática en Go que deberías seguir siempre.

El helper GetRequestID es un patrón que verás repetido: un middleware inyecta un valor en el context y expone una función pública para extraerlo. Así el código que consume el valor no necesita conocer la clave interna.


Middleware de autenticación: validación JWT

La autenticación es donde los middlewares brillan de verdad. En lugar de validar el token en cada handler, centralizas la lógica en un middleware que rechaza las peticiones no autenticadas antes de que lleguen a tu código de negocio.

package middleware

import (
    "context"
    "net/http"
    "strings"

    "github.com/golang-jwt/jwt/v5"
)

type Claims struct {
    UserID string `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

const UserClaimsKey contextKey = "user_claims"

func Auth(secret []byte) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" {
                http.Error(w, `{"error": "missing authorization header"}`, http.StatusUnauthorized)
                return
            }

            // Esperamos "Bearer <token>"
            parts := strings.SplitN(authHeader, " ", 2)
            if len(parts) != 2 || parts[0] != "Bearer" {
                http.Error(w, `{"error": "invalid authorization format"}`, http.StatusUnauthorized)
                return
            }

            token, err := jwt.ParseWithClaims(parts[1], &Claims{}, func(t *jwt.Token) (interface{}, error) {
                if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                    return nil, jwt.ErrSignatureInvalid
                }
                return secret, nil
            })
            if err != nil {
                http.Error(w, `{"error": "invalid token"}`, http.StatusUnauthorized)
                return
            }

            claims, ok := token.Claims.(*Claims)
            if !ok || !token.Valid {
                http.Error(w, `{"error": "invalid token claims"}`, http.StatusUnauthorized)
                return
            }

            ctx := context.WithValue(r.Context(), UserClaimsKey, claims)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

func GetUserClaims(ctx context.Context) *Claims {
    if claims, ok := ctx.Value(UserClaimsKey).(*Claims); ok {
        return claims
    }
    return nil
}

Puntos importantes:

  • El middleware corta la cadena si el token no es válido. Llama a http.Error y hace return sin llamar a next.ServeHTTP. Esto es clave: el handler nunca se ejecuta para peticiones no autenticadas.
  • Validamos el método de firma. Verificamos que el token usa HMAC y no otro algoritmo. Esto previene ataques de confusión de algoritmo donde un atacante cambia el header del JWT a none o a RSA.
  • Las claims van al context. Cualquier handler posterior puede llamar a GetUserClaims(r.Context()) para acceder al usuario autenticado sin parsear el token de nuevo.

Para una implementación más completa de una API con autenticación, revisa API REST con Go.

Middleware de autorización por rol

Una vez tienes la autenticación, la autorización se vuelve trivial como otro middleware:

func RequireRole(roles ...string) func(http.Handler) http.Handler {
    allowed := make(map[string]bool)
    for _, r := range roles {
        allowed[r] = true
    }

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            claims := GetUserClaims(r.Context())
            if claims == nil {
                http.Error(w, `{"error": "unauthorized"}`, http.StatusUnauthorized)
                return
            }

            if !allowed[claims.Role] {
                http.Error(w, `{"error": "forbidden"}`, http.StatusForbidden)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

Esto te permite proteger rutas específicas:

adminOnly := middleware.RequireRole("admin")
mux.Handle("DELETE /users/{id}", adminOnly(http.HandlerFunc(handleDeleteUser)))

Separar autenticación y autorización en middlewares distintos es una buena práctica. Cada uno tiene una responsabilidad clara y puedes combinarlos de forma flexible.


Middleware de recuperación: capturando panics

Un panic en un handler no debería tumbar tu servidor entero. El middleware de recovery captura panics, devuelve un 500 al cliente y registra el error para que puedas investigar. Si quieres entender el mecanismo completo de defer, panic y recover, lo explico en errores en Go.

package middleware

import (
    "fmt"
    "log/slog"
    "net/http"
    "runtime/debug"
)

func Recovery(logger *slog.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            defer func() {
                if err := recover(); err != nil {
                    stack := debug.Stack()
                    logger.Error("panic recovered",
                        slog.String("error", fmt.Sprintf("%v", err)),
                        slog.String("stack", string(stack)),
                        slog.String("method", r.Method),
                        slog.String("path", r.URL.Path),
                    )

                    http.Error(w,
                        `{"error": "internal server error"}`,
                        http.StatusInternalServerError,
                    )
                }
            }()

            next.ServeHTTP(w, r)
        })
    }
}

La clave es el defer con recover(). Cuando un panic ocurre dentro de next.ServeHTTP, el defer se ejecuta, recover() captura el valor del panic, y en lugar de que el programa termine, devolvemos un error 500 controlado.

Incluimos el stack trace completo con debug.Stack(). En producción esto es oro: sin el stack trace, un panic recuperado es un misterio. Con él, puedes ir directamente a la línea que lo causó.

Un middleware de recovery NO es excusa para no manejar errores correctamente. Es una red de seguridad, no tu estrategia de manejo de errores. Si tu código paniquea frecuentemente, el problema está en el código, no en la ausencia de recovery.


Middleware CORS

Si tu API es consumida por un frontend en un dominio diferente, necesitas CORS. Puedes usar una librería como rs/cors, pero entender cómo funciona a nivel de middleware es útil.

package middleware

import "net/http"

type CORSConfig struct {
    AllowedOrigins []string
    AllowedMethods []string
    AllowedHeaders []string
    MaxAge         int
}

func CORS(config CORSConfig) func(http.Handler) http.Handler {
    origins := make(map[string]bool)
    for _, o := range config.AllowedOrigins {
        origins[o] = true
    }

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            origin := r.Header.Get("Origin")

            if origins["*"] || origins[origin] {
                w.Header().Set("Access-Control-Allow-Origin", origin)
            }

            w.Header().Set("Access-Control-Allow-Methods",
                joinStrings(config.AllowedMethods))
            w.Header().Set("Access-Control-Allow-Headers",
                joinStrings(config.AllowedHeaders))

            if config.MaxAge > 0 {
                w.Header().Set("Access-Control-Max-Age",
                    fmt.Sprintf("%d", config.MaxAge))
            }

            // Preflight requests: responder directamente
            if r.Method == http.MethodOptions {
                w.WriteHeader(http.StatusNoContent)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

func joinStrings(ss []string) string {
    result := ""
    for i, s := range ss {
        if i > 0 {
            result += ", "
        }
        result += s
    }
    return result
}

El punto crítico es el manejo de las peticiones OPTIONS (preflight). El navegador envía una petición OPTIONS antes de la petición real para verificar si el servidor permite el acceso cross-origin. Si no la manejas, tu API rechazará todas las peticiones desde el frontend.

En producción, yo recomendaría usar github.com/rs/cors en lugar de implementar CORS desde cero. Las reglas de CORS son más sutiles de lo que parecen y una implementación incompleta puede crear agujeros de seguridad. Pero entender qué hace el middleware por debajo te ayuda a depurar problemas cuando las peticiones fallan sin razón aparente.


Encadenando middlewares: el orden importa

Cuando tienes varios middlewares, el orden en que los encadenas determina el orden de ejecución. Y el orden de ejecución importa mucho.

func main() {
    logger := slog.Default()
    mux := http.NewServeMux()

    mux.HandleFunc("GET /health", handleHealth)
    mux.HandleFunc("GET /users/{id}", handleGetUser)
    mux.HandleFunc("POST /users", handleCreateUser)

    // El orden de aplicación es de fuera hacia dentro.
    // El primer middleware en la cadena es el primero en ejecutarse.
    var handler http.Handler = mux
    handler = middleware.Logging(logger)(handler)  // 3. Log de cada petición
    handler = middleware.Auth(jwtSecret)(handler)   // 2. Verificar autenticación
    handler = middleware.RequestID(handler)          // 1. Generar request ID
    handler = middleware.Recovery(logger)(handler)   // 0. Capturar panics

    http.ListenAndServe(":8080", handler)
}

Lee la cadena de abajo hacia arriba: Recovery es el más externo (se ejecuta primero), seguido de RequestID, luego Auth y finalmente Logging. Esto significa:

  1. Recovery envuelve todo. Si cualquier middleware o handler paniquea, lo captura.
  2. RequestID genera el ID antes de que el resto de middlewares lo necesite.
  3. Auth valida el token. Si falla, ni Logging ni el handler se ejecutan.
  4. Logging registra la petición justo antes/después del handler real.

Un helper para encadenar

Escribir la cadena manualmente es tedioso y propenso a errores. Un helper simple lo resuelve:

func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    // Aplicar en orden inverso para que el primer middleware
    // sea el más externo (se ejecuta primero)
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

Ahora el código queda más legible:

handler := middleware.Chain(mux,
    middleware.Recovery(logger),
    middleware.RequestID,
    middleware.Auth(jwtSecret),
    middleware.Logging(logger),
)

El primer middleware de la lista es el más externo. Mucho más claro.


Middlewares con Gin vs net/http

Hasta ahora todo ha sido con net/http estándar. Si usas Gin, el concepto es el mismo pero la API es ligeramente diferente.

Middlewares en Gin

Gin usa su propio tipo gin.HandlerFunc en lugar de http.Handler:

func LoggingMiddleware(logger *slog.Logger) gin.HandlerFunc {
    return func(c *gin.Context) {
        start := time.Now()

        // Ejecutar el siguiente handler
        c.Next()

        logger.Info("request completed",
            slog.String("method", c.Request.Method),
            slog.String("path", c.Request.URL.Path),
            slog.Int("status", c.Writer.Status()),
            slog.Duration("duration", time.Since(start)),
        )
    }
}

Las diferencias principales:

  • En Gin usas c.Next() en lugar de next.ServeHTTP(w, r).
  • El status code ya está disponible en c.Writer.Status() sin necesidad de envolver el writer.
  • Para abortar la cadena usas c.Abort() o c.AbortWithStatusJSON() en lugar de hacer return sin llamar a next.
func AuthMiddleware(secret []byte) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "missing authorization header",
            })
            return
        }

        // ... validar token ...

        c.Set("user_claims", claims)
        c.Next()
    }
}

Registrar middlewares en Gin

Gin tiene métodos dedicados para registrar middlewares a nivel global o por grupo de rutas:

func main() {
    r := gin.New() // gin.New() sin middlewares por defecto

    // Middlewares globales
    r.Use(gin.Recovery())
    r.Use(LoggingMiddleware(slog.Default()))

    // Grupo público
    public := r.Group("/api")
    {
        public.GET("/health", handleHealth)
        public.POST("/login", handleLogin)
    }

    // Grupo autenticado
    auth := r.Group("/api")
    auth.Use(AuthMiddleware(jwtSecret))
    {
        auth.GET("/users/:id", handleGetUser)
        auth.POST("/users", handleCreateUser)
    }

    // Grupo admin
    admin := r.Group("/api/admin")
    admin.Use(AuthMiddleware(jwtSecret))
    admin.Use(RequireRoleMiddleware("admin"))
    {
        admin.DELETE("/users/:id", handleDeleteUser)
    }

    r.Run(":8080")
}

Esto es más expresivo que con net/http puro. Los grupos de rutas con middlewares específicos son uno de los puntos fuertes de Gin. En net/http puedes lograr lo mismo, pero requiere más código manual.

Cuándo usar cada uno

  • net/http: si tu proyecto es pequeño, si quieres cero dependencias, o si los middlewares del ecosistema estándar te bastan. Desde Go 1.22, el mux estándar soporta patrones como GET /users/{id}, lo que reduce la necesidad de un router externo.
  • Gin: si necesitas routing avanzado con grupos, middleware por grupo, binding de parámetros, o ya tienes un proyecto con Gin. Más detalle en Go y Gin.

Cuándo los middlewares ayudan vs cuándo esconden demasiado

Los middlewares son una herramienta poderosa. Y como toda herramienta poderosa, se pueden usar mal. Después de mantener varias APIs en producción, tengo opiniones claras sobre dónde están los límites.

Buenos usos de middlewares

  • Logging y métricas: la preocupación transversal por excelencia. Cada petición debería generar logs y métricas, y ningún handler debería tener que preocuparse por ello.
  • Autenticación: validar tokens es una preocupación de infraestructura, no de negocio. Centralízala.
  • Request ID y trazabilidad: inyectar IDs de correlación en el context es exactamente lo que los middlewares hacen bien.
  • Recovery: la red de seguridad contra panics. Siempre debería estar presente.
  • CORS: configuración que aplica a todas las peticiones.
  • Rate limiting: control de tráfico a nivel de infraestructura.
  • Compresión: gzip de respuestas que no tiene nada que ver con tu lógica de negocio.

Malos usos de middlewares

  • Lógica de negocio: si tu middleware decide si un usuario puede acceder a un recurso específico basándose en reglas de negocio complejas, eso no es un middleware. Es lógica de negocio escondida en una capa que nadie mira cuando depura.
  • Transformación de datos: si tu middleware modifica el body de la petición o la respuesta de formas no triviales, estás creando magia invisible. El siguiente desarrollador que lea el handler no entenderá por qué los datos no son los que espera.
  • Middlewares con estado mutable compartido: un middleware que escribe en un mapa compartido sin sincronización es un race condition esperando a explotar.
  • Demasiados middlewares en cadena: si tienes quince middlewares, tu petición pasa por quince capas de indirección antes de llegar al handler. Cada capa añade complejidad al debugging. Más de cinco o seis middlewares globales debería hacerte pensar si algunos deberían ser específicos de ciertas rutas.

La regla es simple: si el middleware maneja una preocupación de infraestructura que aplica a muchas rutas, es un buen middleware. Si maneja lógica que solo aplica a un handler específico, ponlo en el handler.


Testing de middlewares

Los middlewares son funciones puras en el sentido de que reciben un handler y devuelven un handler. Esto los hace sorprendentemente fáciles de testear.

Test del middleware de logging

func TestLoggingMiddleware(t *testing.T) {
    // Handler dummy que devuelve 200
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("ok"))
    })

    var buf bytes.Buffer
    logger := slog.New(slog.NewJSONHandler(&buf, nil))

    // Aplicar middleware
    wrapped := Logging(logger)(handler)

    // Crear petición y recorder
    req := httptest.NewRequest(http.MethodGet, "/users/123", nil)
    rec := httptest.NewRecorder()

    wrapped.ServeHTTP(rec, req)

    // Verificar respuesta
    if rec.Code != http.StatusOK {
        t.Errorf("expected status 200, got %d", rec.Code)
    }

    // Verificar que se generó un log
    if buf.Len() == 0 {
        t.Error("expected log output, got none")
    }

    logOutput := buf.String()
    if !strings.Contains(logOutput, "/users/123") {
        t.Errorf("expected log to contain path, got: %s", logOutput)
    }
}

Test del middleware de autenticación

func TestAuthMiddleware_ValidToken(t *testing.T) {
    secret := []byte("test-secret")

    // Crear un token válido
    claims := &Claims{
        UserID: "user-1",
        Role:   "admin",
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString(secret)
    if err != nil {
        t.Fatal(err)
    }

    // Handler que verifica que las claims están en el context
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userClaims := GetUserClaims(r.Context())
        if userClaims == nil {
            t.Error("expected claims in context")
            return
        }
        if userClaims.UserID != "user-1" {
            t.Errorf("expected user-1, got %s", userClaims.UserID)
        }
        w.WriteHeader(http.StatusOK)
    })

    wrapped := Auth(secret)(handler)

    req := httptest.NewRequest(http.MethodGet, "/", nil)
    req.Header.Set("Authorization", "Bearer "+tokenString)
    rec := httptest.NewRecorder()

    wrapped.ServeHTTP(rec, req)

    if rec.Code != http.StatusOK {
        t.Errorf("expected 200, got %d", rec.Code)
    }
}

func TestAuthMiddleware_MissingHeader(t *testing.T) {
    secret := []byte("test-secret")

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        t.Error("handler should not be called")
    })

    wrapped := Auth(secret)(handler)

    req := httptest.NewRequest(http.MethodGet, "/", nil)
    rec := httptest.NewRecorder()

    wrapped.ServeHTTP(rec, req)

    if rec.Code != http.StatusUnauthorized {
        t.Errorf("expected 401, got %d", rec.Code)
    }
}

Test del middleware de recovery

func TestRecoveryMiddleware(t *testing.T) {
    var buf bytes.Buffer
    logger := slog.New(slog.NewJSONHandler(&buf, nil))

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        panic("something went terribly wrong")
    })

    wrapped := Recovery(logger)(handler)

    req := httptest.NewRequest(http.MethodGet, "/", nil)
    rec := httptest.NewRecorder()

    // No debería paniquear
    wrapped.ServeHTTP(rec, req)

    if rec.Code != http.StatusInternalServerError {
        t.Errorf("expected 500, got %d", rec.Code)
    }

    if !strings.Contains(buf.String(), "something went terribly wrong") {
        t.Error("expected panic message in logs")
    }
}

El patrón es siempre el mismo:

  1. Crea un handler dummy (que simula el comportamiento que quieres testear).
  2. Aplica el middleware.
  3. Usa httptest.NewRequest y httptest.NewRecorder.
  4. Verifica el resultado.

httptest es parte de la librería estándar de Go. No necesitas mocks externos ni frameworks de testing. Todo viene incluido. Si quieres profundizar en testing en Go, tengo un artículo dedicado: testing en Go.


Funciones que envuelven funciones, nada más

Los middlewares en Go son funciones que envuelven handlers. No hay magia, no hay anotaciones, no hay framework obligatorio. Y esa simplicidad es precisamente lo que los hace tan efectivos. La firma func(http.Handler) http.Handler es todo lo que necesitas para logging, request IDs, autenticación, autorización, recovery y CORS. Se encadenan, se componen, se testean con httptest y funcionan igual en net/http que en Gin con mínimos cambios de API.

La regla que te va a ahorrar problemas: usa middlewares para preocupaciones transversales de infraestructura. Si algo huele a lógica de negocio, no es un middleware. Es código que debería estar en un handler o en un servicio, visible y explícito.

Empieza con logging, request ID y recovery. Añade autenticación cuando la necesites. Y resiste la tentación de meter más lógica de la necesaria en la cadena de middlewares. Tu futuro yo depurando a las tres de la mañana te lo agradecerá.

OshyTech

Ingeniería backend y de datos orientada a sistemas escalables, automatización e IA.

Navegación

Copyright 2026 OshyTech. Todos los derechos reservados