Toda API Go de produção tem o mesmo conjunto de preocupações transversais: logar cada request, recuperar panic, propagar context, validar token, limitar taxa, adicionar headers de segurança e medir latência. Se você espalha isso em cada handler, o código vira um copiar-e-colar frágil. Se você concentra em middleware, cada rota declara só o que é de negócio e a chain cuida do resto.
Este guia mostra o padrão idiomático de middleware em Go — func(http.Handler) http.Handler — com net/http puro, Chi e Gin, além dos middlewares que quase toda API no Brasil precisa em 2026. Ele complementa o comparativo de frameworks HTTP, o guia de rate limiting, o guia de autenticação e autorização, o logging com slog, o context com timeout e o tutorial de API REST.
O que é middleware (na definição de Go)
Em Go, middleware não é um plugin mágico do framework. É uma função que recebe um http.Handler e devolve outro http.Handler. O handler devolvido faz algo antes de chamar o próximo, depois, ou os dois:
type Middleware func(http.Handler) http.Handler
O handler final continua sendo o que você já conhece:
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"ok":true}`))
})
A beleza do padrão é a composição. Como tudo é http.Handler, você empilha middlewares sem o framework saber (ou precisar saber) o que cada um faz. Isso é o mesmo espírito do clean architecture sem overengineering: interfaces pequenas, composição explícita.
O padrão canônico: wrapping
O middleware mais simples possível só loga e repassa:
package middleware
import (
"log"
"net/http"
"time"
)
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
Para usar com net/http puro:
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", healthz)
var handler http.Handler = mux
handler = Logging(handler)
srv := &http.Server{
Addr: ":8080",
Handler: handler,
}
A ordem importa. Se você aplicar A(B(C(mux))), a requisição passa por A → B → C → mux, e a resposta sobe C → B → A. Pense em cebola: o middleware mais externo vê a request primeiro e a response por último.
Chain: montar a pilha sem dor
Em vez de aninhar na mão, um helper de chain deixa a ordem legível. A convenção comum é aplicar da esquerda para a direita, com o primeiro da lista sendo o mais externo:
package middleware
import "net/http"
func Chain(h http.Handler, mws ...func(http.Handler) http.Handler) http.Handler {
// aplica do último para o primeiro para que mws[0] fique por fora
for i := len(mws) - 1; i >= 0; i-- {
h = mws[i](h)
}
return h
}
Uso em produção:
handler := middleware.Chain(
mux,
middleware.RequestID,
middleware.Recover,
middleware.Logging,
middleware.CORS(allowedOrigins),
)
Essa stack é o mínimo que eu esperaria de qualquer serviço exposto na internet. Recovery e request ID quase sempre ficam por fora do logging, para que panics e correlação apareçam no log.
Recovery: o middleware que salva o processo
Um panic em handler, sem recovery, encerra o processo. Em Kubernetes isso vira restart; sob carga, pode virar cascata. O recovery captura, loga e responde 500:
package middleware
import (
"log/slog"
"net/http"
"runtime/debug"
)
func Recover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
slog.ErrorContext(r.Context(), "panic recuperado",
"err", rec,
"stack", string(debug.Stack()),
"path", r.URL.Path,
"method", r.Method,
)
http.Error(w, `{"error":"internal server error"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
Combine com slog estruturado e, se já tiver, com o request ID no contexto. Em incidentes, stack + request_id + path resolvem 80% da triagem.
Request ID: correlação de logs
Sem identificador por requisição, correlacionar logs de um timeout em produção é caça ao tesouro. O middleware gera (ou propaga) um ID e coloca no contexto e no header de resposta:
package middleware
import (
"context"
"net/http"
"github.com/google/uuid"
)
type ctxKey int
const requestIDKey ctxKey = 1
func RequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := r.Header.Get("X-Request-ID")
if id == "" {
id = uuid.NewString()
}
ctx := context.WithValue(r.Context(), requestIDKey, id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func GetRequestID(ctx context.Context) string {
if v, ok := ctx.Value(requestIDKey).(string); ok {
return v
}
return ""
}
Propague o mesmo header para serviços downstream (HTTP client, gRPC metadata). Isso fecha o circuito com OpenTelemetry e com o guia de context: o context.Context carrega cancelamento e valores de correlação.
Logging de request com status e bytes
O ResponseWriter padrão não expõe o status code depois do WriteHeader. Para logar status e bytes escritos, envolva o writer:
package middleware
import (
"log/slog"
"net/http"
"time"
)
type statusWriter struct {
http.ResponseWriter
status int
bytes int
}
func (w *statusWriter) WriteHeader(code int) {
w.status = code
w.ResponseWriter.WriteHeader(code)
}
func (w *statusWriter) Write(b []byte) (int, error) {
if w.status == 0 {
w.status = http.StatusOK
}
n, err := w.ResponseWriter.Write(b)
w.bytes += n
return n, err
}
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(sw, r)
slog.InfoContext(r.Context(), "request",
"method", r.Method,
"path", r.URL.Path,
"status", sw.status,
"bytes", sw.bytes,
"duration_ms", time.Since(start).Milliseconds(),
"request_id", GetRequestID(r.Context()),
"remote", r.RemoteAddr,
)
})
}
Em produção, evite logar body ou query string com dados sensíveis (tokens, CPF, cartão). Prefira campos estruturados e sampling em rotas de health check de alto volume — o health check guide explica por que /healthz não deve poluir o log.
CORS, auth e rate limit como middleware
Três middlewares aparecem em quase toda API pública ou SPA+backend no Brasil:
CORS
func CORS(origins map[string]struct{}) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if _, ok := origins[origin]; ok {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-ID")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
Nunca use Access-Control-Allow-Origin: * junto com cookies ou Authorization em APIs autenticadas. Liste origens explicitamente.
Autenticação
O middleware de auth valida o Bearer token, coloca o usuário no contexto e deixa a autorização (pode editar este recurso?) para o handler ou para um middleware de grupo. Detalhes de JWT, refresh e RBAC estão no guia de autenticação e no tutorial de JWT:
func Auth(validate func(*http.Request) (userID string, err error)) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
uid, err := validate(r)
if err != nil {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userKey, uid)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
Rate limiting
Rate limit é middleware por excelência: corta abuso antes do handler tocar banco ou fila. Implementações com token bucket, Redis e limites por IP/API key estão no guia de rate limiting em produção. Na chain, ele costuma ficar depois de request ID e antes de auth pesada, ou depois de auth se o limite for por usuário.
Middleware no Chi
Chi é 100% http.Handler. Por isso o middleware que você escreveu acima funciona sem adaptador:
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Recover)
r.Use(middleware.Logging)
r.Get("/healthz", healthz)
r.Group(func(r chi.Router) {
r.Use(middleware.Auth(validateToken))
r.Get("/me", getMe)
r.Post("/orders", createOrder)
})
Use aplica a todos os handlers daquele router/grupo; With aplica só a uma rota. Grupos são a forma correta de separar rotas públicas de privadas sem if dentro do middleware global.
Middleware no Gin (e a diferença de tipo)
Gin não usa http.Handler na chain pública. O tipo é gin.HandlerFunc, e a continuação é c.Next():
func GinRequestID() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.GetHeader("X-Request-ID")
if id == "" {
id = uuid.NewString()
}
c.Set("request_id", id)
c.Writer.Header().Set("X-Request-ID", id)
c.Next()
}
}
func main() {
r := gin.New()
r.Use(GinRequestID(), gin.Recovery(), gin.Logger())
r.GET("/healthz", healthz)
r.Run(":8080")
}
Você pode adaptar http.Handler middleware para Gin com gin.WrapH / gin.WrapF, mas a ida e volta tem custo de ergonomia. Se o time já é Gin, mantenha a chain em gin.HandlerFunc. Se ainda está escolhendo framework, o comparativo Gin/Echo/Fiber/Chi pesa exatamente isso: Chi reutiliza o ecossistema net/http; Fiber, por cima de fasthttp, não.
net/http 1.22+, timeouts e graceful shutdown
Middleware não substitui configuração do servidor. Timeouts de leitura/escrita e shutdown gracioso continuam no http.Server e no processo:
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
Sem ReadHeaderTimeout, conexões lentas seguram workers. O encerramento limpo — drenar requests, fechar listeners, flush de logs — está no guia de graceful shutdown. Middleware de logging e recovery continuam ativos até o último request drenado.
Ordem recomendada da chain
Uma ordem que funciona bem na maioria das APIs:
| Posição | Middleware | Por quê |
|---|---|---|
| 1 (externo) | Request ID | Tudo que logar depois já tem correlação |
| 2 | Recover | Panic não mata o processo; log leva o ID |
| 3 | Logging | Mede duração real, inclusive de middlewares internos |
| 4 | CORS / security headers | Responde OPTIONS cedo, sem auth |
| 5 | Rate limit | Corta abuso barato |
| 6 | Auth | Só quem passou do limite gasta validação de token |
| 7 (interno) | Handler de negócio | Só regra de domínio |
Ajustes: se o rate limit for por usuário, auth precisa vir antes. Se for por IP na borda, rate limit antes de auth economiza CPU. Documente a escolha no README do serviço.
Armadilhas comuns
- Esquecer de chamar
next: middleware que autentica e não chama o próximo em caso de sucesso é bug silencioso (a request “trava” sem response em alguns caminhos). Sempre chamenextou escreva a response de erro ereturn. - Mutar
*http.RequestsemWithContext: o request é compartilhado; o idiomático ér = r.WithContext(ctx)e passar adiante. - Bufferizar body inteiro em middleware de log: em upload grande isso estoura memória. Logue metadata; deixe body para o handler. Veja também upload multipart e S3.
- Middleware global demais: auth global em
/healthze/metricsquebra probes do Kubernetes. Use grupos (Chi) our.Group(Gin). - Depender de Fiber middleware em biblioteca interna: se um dia o time migrar para Chi/
net/http, você reescreve tudo. Prefirafunc(http.Handler) http.Handlerem libs compartilhadas. - Logar 100% de
/healthz: em cluster grande isso domina o volume. Ignore paths de probe no logger ou use sampling.
Testando middleware
Middleware é função pura o bastante para testar com httptest:
func TestRecover(t *testing.T) {
h := Recover(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
panic("boom")
}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != http.StatusInternalServerError {
t.Fatalf("status = %d", rr.Code)
}
}
Para auth, injete um validate fake. Para logging, use um slog.Handler em buffer. O guia de mocks e httptest aprofunda o padrão.
Quando não usar middleware
Middleware é péssimo para regra de negócio de um único endpoint (“se o pedido for do tipo X e o estoque Y…”). Isso é handler. Middleware brilha em políticas transversais. Também não use middleware para esconder dependências enormes (banco, fila) sem injeção clara — prefira constructors que recebem interfaces, no espírito de dependency injection sem framework.
Perguntas frequentes
O que é middleware em Go?
Middleware em Go é uma função que envolve um http.Handler e devolve outro http.Handler. Ele intercepta a requisição antes (e/ou a resposta depois) do handler final: log, autenticação, recovery de panic, CORS, rate limit e request ID são os casos clássicos. O padrão idiomático é func(http.Handler) http.Handler.
Como encadear middleware com net/http puro?
Compose funções do tipo func(http.Handler) http.Handler. A forma mais clara é aplicar de dentro para fora ou usar um helper Chain. Com Chi, Use e With montam a chain de forma declarativa. Em Gin e Echo a API é similar, mas o tipo do handler muda (gin.HandlerFunc em vez de http.Handler).
Middleware de recovery é obrigatório em produção?
Sim. Um panic não capturado em um handler derruba o processo inteiro se não houver recovery. O middleware de recovery deve recuperar o panic, logar o stack com slog ou equivalente, devolver 500 e deixar o processo vivo. Em Kubernetes isso evita restart em cascata por um bug pontual.
Onde colocar autenticação: middleware ou handler?
Validação do token e extração de identidade costumam ficar no middleware de autenticação. Autorização fina (este usuário pode editar este recurso?) fica no handler ou em um middleware de autorização por rota/grupo. Separe autenticação (quem é) de autorização (o que pode). Detalhes em autenticação e autorização em APIs Go.
Chi, Gin ou net/http: qual middleware usar?
Se você usa net/http ou Chi, escreva middleware no padrão http.Handler — reutilizável em qualquer router compatível. Em Gin, o tipo é gin.HandlerFunc e a chain é c.Next(). Fiber não usa net/http; o middleware dele não é portável para o ecossistema padrão. Prefira o padrão da standard library quando puder. O comparativo de frameworks ajuda a decidir o router; este guia define o padrão de middleware em cima dessa escolha.
Conclusão
Middleware em Go é composição de http.Handler, não mágica de framework. Domine o padrão func(http.Handler) http.Handler, monte uma chain com request ID, recovery, logging, CORS, rate limit e auth, e use grupos de rota para não autenticar health checks. Com Chi você reutiliza o ecossistema net/http; com Gin você adapta o mesmo desenho à gin.HandlerFunc.
Próximos passos naturais na malha de produção do site:
- Frameworks HTTP: Gin, Echo, Fiber, Chi
- Rate limiting em APIs Go
- Autenticação e autorização
- slog: logging estruturado
- Context, timeout e cancelamento
- Graceful shutdown
- Health checks
- API REST com Go
- Vagas de Go no Brasil
Se você está montando portfólio ou se preparando para a primeira vaga, implemente essa chain do zero em um serviço pequeno — é um dos projetos que mais conversam bem em entrevista de backend Go.