Middlewares en Go: logging, auth, errors i recuperació

Com funcionen els middlewares en Go amb exemples de logging, autenticació, request ID, recuperació de panic i mètriques.

Cover for Middlewares en Go: logging, auth, errors i recuperació

Els middlewares en Go són l’equivalent dels filtres de Spring o dels middlewares d’Express. Un concepte simple: una funció que embolcalla una altra funció i executa lògica abans, després o al voltant de cada petició HTTP. Quan s’utilitzen bé, eliminen duplicació i centralitzen preocupacions transversals. Quan s’utilitzen malament, amaguen lògica de negoci en capes invisibles i converteixen el debugging en un malson.

En Go, un middleware no és una anotació màgica ni un framework. És una funció que rep un handler i en retorna un altre. Res més. I aquesta simplicitat és exactament el que el fa tan potent.


Què és un middleware en Go

Un middleware en Go és una funció que embolcalla un http.Handler o un http.HandlerFunc. La seva feina és interceptar la petició abans que arribi al handler real, executar alguna lògica (logging, autenticació, mètriques), i decidir si la petició continua o es talla.

El patró és sempre el mateix:

func ElMeuMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Lògica abans del handler
        next.ServeHTTP(w, r)
        // Lògica després del handler
    })
}

Això és tot. No hi ha interfícies que implementar, no hi ha classes abstractes, no hi ha registre en un contenidor. Una funció que rep un handler, en retorna un, i al mig fa el que necessitis.

La signatura func(http.Handler) http.Handler és la convenció estàndard. Si veus aquesta signatura en qualsevol llibreria de Go, saps que és un middleware compatible amb net/http. Això és important perquè significa que els middlewares són composables: pots encadenar deu middlewares de diferents llibreries i tots funcionen junts sense adaptadors.

Si véns de Spring, pensa en un HandlerInterceptor però sense @Component, sense @Order, sense XML. Si véns d’Express, és exactament el mateix concepte però tipat i sense next() que pots oblidar de cridar… bé, en Go també te’l pots oblidar, però el compilador almenys t’ajuda amb el tipus.


El teu primer middleware: logging de durada

El middleware més útil i el primer que hauries d’escriure en qualsevol projecte és un que registri quant tarda cada petició. És simple, té un valor immediat en producció i serveix com a plantilla per entendre el patró.

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()

            // Embolcallar el ResponseWriter per 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)
}

Hi ha un detall important aquí: el responseWriter embolcallador. El http.ResponseWriter estàndard no exposa el status code després que s’escriu. Necessites interceptar la crida a WriteHeader per capturar-lo. Faràs servir aquest patró en molts middlewares.

Fixa’t també que el middleware rep un *slog.Logger com a paràmetre. Això és injecció de dependències a l’estil Go: passes el que necessites com a argument. El middleware retorna una altra funció que és el middleware real. Aquest patró de “factory function” és estàndard quan el teu middleware necessita configuració.

Per usar-lo:

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ó ara genera un log estructurat amb mètode, path, status code i durada. Sense tocar ni una línia dels handlers.


Middleware de Request ID

En producció, quan tens centenars de peticions per segon, necessites poder traçar una petició específica a través de tots els teus logs. El request ID és la manera estàndard de fer-ho: generes un identificador únic per a cada petició i el 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 client ja envia un request ID, el reutilitzem
        id := r.Header.Get(\"X-Request-ID\")
        if id == \"\" {
            id = uuid.New().String()
        }

        // Afegir al context perquè l'usin altres middlewares i handlers
        ctx := context.WithValue(r.Context(), RequestIDKey, id)

        // Afegir al header de resposta perquè el client pugui traçar-lo
        w.Header().Set(\"X-Request-ID\", id)

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

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

L’ús de context.WithValue és clau aquí. El context en Go és el mecanisme estàndard per passar valors a través de la cadena de handlers sense contaminar les signatures de les funcions. El request ID és un dels casos d’ús més legítims per a context.WithValue.

Un detall que val la pena mencionar: fem servir un tipus contextKey privat com a clau del context. Això evita col·lisions amb altres paquets que poguessin usar \"request_id\" com a clau. És una convenció idiomàtica en Go que hauríeu de seguir sempre.

El helper GetRequestID és un patró que veuràs repetit: un middleware injecta un valor al context i exposa una funció pública per extreure’l. Així el codi que consumeix el valor no necessita conèixer la clau interna.


Middleware d’autenticació: validació JWT

L’autenticació és on els middlewares brillen de debò. En lloc de validar el token a cada handler, centralitzes la lògica en un middleware que rebutja les peticions no autenticades abans que arribin al teu codi de negoci.

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
            }

            // Esperem \"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
}

Punts importants:

  • El middleware talla la cadena si el token no és vàlid. Crida http.Error i fa return sense cridar next.ServeHTTP. Això és clau: el handler mai s’executa per a peticions no autenticades.
  • Validem el mètode de signatura. Verifiquem que el token usa HMAC i no un altre algorisme. Això prevé atacs de confusió d’algorisme on un atacant canvia la capçalera del JWT a none o a RSA.
  • Les claims van al context. Qualsevol handler posterior pot cridar GetUserClaims(r.Context()) per accedir a l’usuari autenticat sense tornar a analitzar el token.

Per a una implementació més completa d’una API amb autenticació, consulta API REST amb Go.

Middleware d’autorització per rol

Un cop tens l’autenticació, l’autorització es torna trivial com un altre 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)
        })
    }
}

Això et permet protegir rutes específiques:

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

Separar autenticació i autorització en middlewares diferents és una bona pràctica. Cadascun té una responsabilitat clara i pots combinar-los de forma flexible.


Middleware de recuperació: capturant panics

Un panic en un handler no hauria d’enfonsar el teu servidor sencer. El middleware de recovery captura panics, retorna un 500 al client i registra l’error perquè puguis investigar. Si vols entendre el mecanisme complet de defer, panic i recover, ho explico a errors 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 clau és el defer amb recover(). Quan un panic ocorre dins de next.ServeHTTP, el defer s’executa, recover() captura el valor del panic, i en lloc que el programa acabi, retornem un error 500 controlat.

Incloem el stack trace complet amb debug.Stack(). En producció això és or: sense el stack trace, un panic recuperat és un misteri. Amb ell, pots anar directament a la línia que el va causar.

Un middleware de recovery NO és una excusa per no gestionar errors correctament. És una xarxa de seguretat, no la teva estratègia de gestió d’errors. Si el teu codi entra en panic freqüentment, el problema és al codi, no en l’absència de recovery.


Middleware CORS

Si la teva API és consumida per un frontend en un domini diferent, necessites CORS. Pots usar una llibreria com rs/cors, però entendre com funciona a nivell de middleware és ú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))
            }

            // Peticions preflight: respondre directament
            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 punt crític és la gestió de les peticions OPTIONS (preflight). El navegador envia una petició OPTIONS abans de la petició real per verificar si el servidor permet l’accés cross-origin. Si no la gestiones, la teva API rebutjarà totes les peticions des del frontend.

En producció, recomanaria usar github.com/rs/cors en lloc d’implementar CORS des de zero. Les regles de CORS són més subtils del que semblen i una implementació incompleta pot crear forats de seguretat. Però entendre què fa el middleware per sota t’ajuda a depurar problemes quan les peticions fallen sense raó aparent.


Encadenant middlewares: l’ordre importa

Quan tens diversos middlewares, l’ordre en què els encadenes determina l’ordre d’execució. I l’ordre d’execució importa molt.

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

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

    // L'ordre d'aplicació és de fora cap a dins.
    // El primer middleware de la cadena és el primer en executar-se.
    var handler http.Handler = mux
    handler = middleware.Logging(logger)(handler)  // 3. Log de cada petició
    handler = middleware.Auth(jwtSecret)(handler)   // 2. Verificar autenticació
    handler = middleware.RequestID(handler)          // 1. Generar request ID
    handler = middleware.Recovery(logger)(handler)   // 0. Capturar panics

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

Llegeix la cadena de baix cap a dalt: Recovery és el més extern (s’executa primer), seguit de RequestID, llavors Auth i finalment Logging. Això significa:

  1. Recovery embolcalla tot. Si qualsevol middleware o handler entra en panic, el captura.
  2. RequestID genera l’ID abans que la resta de middlewares el necessitin.
  3. Auth valida el token. Si falla, ni Logging ni el handler s’executen.
  4. Logging registra la petició just abans/després del handler real.

Un helper per encadenar

Escriure la cadena manualment és tediós i propens a errors. Un helper simple ho soluciona:

func Chain(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    // Aplicar en ordre invers perquè el primer middleware
    // sigui el més extern (s'executa primer)
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

Ara el codi queda més llegible:

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

El primer middleware de la llista és el més extern. Molt més clar.


Middlewares amb Gin vs net/http

Fins ara tot ha estat amb net/http estàndard. Si uses Gin, el concepte és el mateix però l’API és lleugerament diferent.

Middlewares en Gin

Gin usa el seu propi tipus gin.HandlerFunc en lloc de http.Handler:

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

        // Executar el següent 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)),
        )
    }
}

Les diferències principals:

  • A Gin uses c.Next() en lloc de next.ServeHTTP(w, r).
  • El status code ja està disponible a c.Writer.Status() sense necessitat d’embolcallar el writer.
  • Per avortar la cadena uses c.Abort() o c.AbortWithStatusJSON() en lloc de fer return sense cridar 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 té mètodes dedicats per registrar middlewares a nivell global o per grup de rutes:

func main() {
    r := gin.New() // gin.New() sense middlewares per defecte

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

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

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

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

    r.Run(\":8080\")
}

Això és més expressiu que amb net/http pur. Els grups de rutes amb middlewares específics són un dels punts forts de Gin. A net/http pots aconseguir el mateix, però requereix més codi manual.

Quan usar cadascun

  • net/http: si el teu projecte és petit, si vols zero dependències, o si els middlewares de l’ecosistema estàndard et basten. Des de Go 1.22, el mux estàndard suporta patrons com GET /users/{id}, cosa que redueix la necessitat d’un router extern.
  • Gin: si necessites routing avançat amb grups, middleware per grup, binding de paràmetres, o ja tens un projecte amb Gin. Més detall a Go i Gin.

Quan els middlewares ajuden vs quan amaguen massa

Els middlewares són una eina poderosa. I com tota eina poderosa, es poden usar malament. Després de mantenir diverses APIs en producció, tinc opinions clares sobre on estan els límits.

Bons usos de middlewares

  • Logging i mètriques: la preocupació transversal per excel·lència. Cada petició hauria de generar logs i mètriques, i cap handler hauria de preocupar-se per això.
  • Autenticació: validar tokens és una preocupació d’infraestructura, no de negoci. Centralitza-la.
  • Request ID i traçabilitat: injectar IDs de correlació al context és exactament el que els middlewares fan bé.
  • Recovery: la xarxa de seguretat contra panics. Sempre hauria d’estar present.
  • CORS: configuració que aplica a totes les peticions.
  • Rate limiting: control de tràfic a nivell d’infraestructura.
  • Compressió: gzip de respostes que no té res a veure amb la teva lògica de negoci.

Mals usos de middlewares

  • Lògica de negoci: si el teu middleware decideix si un usuari pot accedir a un recurs específic basant-se en regles de negoci complexes, això no és un middleware. És lògica de negoci amagada en una capa que ningú mira quan depura.
  • Transformació de dades: si el teu middleware modifica el body de la petició o la resposta de maneres no trivials, estàs creant màgia invisible. El següent desenvolupador que llegeixi el handler no entendrà per què les dades no són les que espera.
  • Middlewares amb estat mutable compartit: un middleware que escriu en un mapa compartit sense sincronització és una condició de carrera esperant explotar.
  • Massa middlewares en cadena: si tens quinze middlewares, la teva petició passa per quinze capes d’indirecta abans d’arribar al handler. Cada capa afegeix complexitat al debugging. Més de cinc o sis middlewares globals hauria de fer-te pensar si alguns haurien de ser específics de certes rutes.

La regla és simple: si el middleware gestiona una preocupació d’infraestructura que aplica a moltes rutes, és un bon middleware. Si gestiona lògica que només aplica a un handler específic, posa-la al handler.


Testing de middlewares

Els middlewares són funcions pures en el sentit que reben un handler i en retornen un. Això els fa sorprenentment fàcils de testejar.

Test del middleware de logging

func TestLoggingMiddleware(t *testing.T) {
    // Handler dummy que retorna 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ó i recorder
    req := httptest.NewRequest(http.MethodGet, \"/users/123\", nil)
    rec := httptest.NewRecorder()

    wrapped.ServeHTTP(rec, req)

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

    // Verificar que s'ha generat 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 d’autenticació

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

    // Crear un token vàlid
    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 les claims estan al 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 hauria d'entrar en panic
    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ó és sempre el mateix:

  1. Crea un handler dummy (que simula el comportament que vols testejar).
  2. Aplica el middleware.
  3. Usa httptest.NewRequest i httptest.NewRecorder.
  4. Verifica el resultat.

httptest és part de la llibreria estàndard de Go. No necessites mocks externs ni frameworks de testing. Tot ve inclòs. Si vols aprofundir en testing a Go, tinc un article dedicat: testing en Go.


Funcions que embolcallen funcions, res més

Els middlewares en Go són funcions que embolcallen handlers. No hi ha màgia, no hi ha anotacions, no hi ha framework obligatori. I aquesta simplicitat és precisament el que els fa tan efectius. La signatura func(http.Handler) http.Handler és tot el que necessites per a logging, request IDs, autenticació, autorització, recovery i CORS. S’encadenen, es composen, es testegen amb httptest i funcionen igual a net/http que a Gin amb mínims canvis d’API.

La regla que t’estalviarà problemes: usa middlewares per a preocupacions transversals d’infraestructura. Si alguna cosa fa olor de lògica de negoci, no és un middleware. És codi que hauria d’estar en un handler o en un servei, visible i explícit.

Comença amb logging, request ID i recovery. Afegeix autenticació quan la necessitis. I resisteix la temptació de posar més lògica de la necessària a la cadena de middlewares. El teu jo futur depurant a les tres de la matinada t”ho agrairà.

Articles relacionats

OshyTech

Enginyeria backend i de dades orientada a sistemes escalables, automatització i IA.

Navegació

Copyright 2026 OshyTech. Tots els drets reservats