Middlewares em Go: Arquitetura Cebola, Interceptação e Produção
Como construir middlewares em Go que envolvem http.Handler, com a arquitetura de cebola, interceptação de requisições e cuidados de produção.
Publicada em · 6 min de leitura
Middlewares em Go: Arquitetura Cebola, Interceptação e Produção
No ecossistema Go, a construção de APIs e microsserviços HTTP baseia-se fortemente na interface padrão http.Handler do pacote net/http. Um Middleware é uma função de alta ordem (higher-order function) que envolve um http.Handler existente, interceptando a requisição antes que ela atinja o manipulador final (handler de negócio) e interceptando a resposta antes que ela seja finalizada no socket de rede.
Ao contrário de outros frameworks que introduzem conceitos proprietários de “filtros” ou “interceptores”, o Go implementa middlewares utilizando pura composição funcional através da assinatura canônica:
$$\text{type Middleware} = \text{func}(\text{http.Handler}) \to \text{http.Handler}$$
1. Arquitetura em Cebola (The Onion Model)
O pipeline de middlewares em Go funciona exatamente como as camadas de uma cebola. Cada camada tem a oportunidade de executar lógica na ida (inbound, antes de chamar next.ServeHTTP) e na volta (outbound, após next.ServeHTTP ter concluído).
Ordem Cronológica de Execução:
- A requisição HTTP chega pela camada mais externa (Logging & Metrics). Um cronômetro de alta precisão é iniciado.
- A requisição desce para a camada de resiliência (Panic Recovery), onde um bloco
defer recover()é registrado na pilha da goroutine. - A requisição passa pela camada de rastreabilidade (Request ID & Context), que injeta um Correlation ID único no
context.Context. - O Handler de Negócio executa a lógica do caso de uso e grava o payload e código de status HTTP.
- O controle retorna subindo pelas camadas: métricas de bytes são registradas, o tempo total de resposta é medido e o log estruturado é emitido.
2. O Desafio do Status Code: wrappedResponseWriter
A interface padrão http.ResponseWriter do Go possui os métodos Header(), Write([]byte) e WriteHeader(int). No entanto, ela não expõe nenhum método de leitura para descobrir qual status code foi gravado ou quantos bytes foram enviados.
Para que um middleware de logging e métricas saiba se o handler respondeu 200 OK, 404 Not Found ou 500 Internal Server Error, usamos o padrão de projeto Decorator, envolvendo o http.ResponseWriter original em uma struct customizada:
Implementação do Wrapper Decorator
package middleware
import "net/http"
// wrappedResponseWriter intercepta chamadas de escrita para capturar métricas
type wrappedResponseWriter struct {
http.ResponseWriter
statusCode int
bytesWritten int64
wroteHeader bool
}
func newWrappedResponseWriter(w http.ResponseWriter) *wrappedResponseWriter {
// 200 OK é o valor default padrão do Go se WriteHeader não for invocado explicitamente
return &wrappedResponseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
}
func (w *wrappedResponseWriter) WriteHeader(statusCode int) {
if !w.wroteHeader {
w.statusCode = statusCode
w.wroteHeader = true
w.ResponseWriter.WriteHeader(statusCode)
}
}
func (w *wrappedResponseWriter) Write(b []byte) (int, error) {
if !w.wroteHeader {
w.WriteHeader(http.StatusOK)
}
n, err := w.ResponseWriter.Write(b)
w.bytesWritten += int64(n)
return n, err
}
3. Os 5 Middlewares Fundamentais de Produção
3.1 Logger Estruturado e Métricas de Latência
Um middleware de observabilidade registra método, rota, endereço IP, código de status e duração em nanossegundos/microssegundos:
package middleware
import (
"log/slog"
"net/http"
"time"
)
func Logger(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()
wrapped := newWrappedResponseWriter(w)
// Executa as camadas internas
next.ServeHTTP(wrapped, r)
// Executa após a conclusão da resposta
duration := time.Since(start)
logger.Info("http_request_finished",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.Int("status", wrapped.statusCode),
slog.Duration("duration_ms", duration),
slog.Int64("bytes", wrapped.bytesWritten),
slog.String("remote_addr", r.RemoteAddr),
slog.String("request_id", GetRequestID(r.Context())),
)
})
}
}
3.2 Panic Recovery: Imunidade Contra Crashes
Em Go, um panic não tratado dentro de uma goroutine derruba todo o processo da aplicação. O middleware de recuperação captura o pânico com recover(), registra o stack trace completo e devolve um JSON 500 Internal Server Error limpo para o cliente:
package middleware
import (
"encoding/json"
"log/slog"
"net/http"
"runtime/debug"
)
func Recoverer(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 rec := recover(); rec != nil {
// Loga o erro crítico e a pilha de chamadas
stack := string(debug.Stack())
logger.Error("panic_recovered",
slog.Any("error", rec),
slog.String("stack", stack),
slog.String("path", r.URL.Path),
)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusInternalServerError)
_ = json.NewEncoder(w).Encode(map[string]string{
"error": "Ocorreu um erro interno inesperado no servidor.",
})
}
}()
next.ServeHTTP(w, r)
})
}
}
3.3 Request ID & Context Propagation
Para rastrear uma requisição ponta a ponta (distributed tracing), cada requisição deve carregar um identificador único. Se o cliente enviar o header X-Request-ID, nós o aproveitamos; caso contrário, geramos um novo:
package middleware
import (
"context"
"crypto/rand"
"encoding/hex"
"net/http"
)
// Tipo privado para evitar colisões no context.Context
type contextKey string
const requestIDKey contextKey = "ctx_request_id"
func RequestID() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
bytes := make([]byte, 16)
_, _ = rand.Read(bytes)
reqID = hex.EncodeToString(bytes)
}
// Injeta no header de resposta para que o cliente saiba o ID
w.Header().Set("X-Request-ID", reqID)
// Injeta no context da requisição
ctx := context.WithValue(r.Context(), requestIDKey, reqID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// GetRequestID recupera o ID de correlação do contexto de forma segura
func GetRequestID(ctx context.Context) string {
if val, ok := ctx.Value(requestIDKey).(string); ok {
return val
}
return ""
}
3.4 CORS (Cross-Origin Resource Sharing)
Navegadores modernos exigem resposta adequada para pre-flight requests (OPTIONS):
package middleware
import "net/http"
type CORSConfig struct {
AllowedOrigins []string
AllowedMethods []string
AllowedHeaders []string
}
func CORS(cfg CORSConfig) 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 origin != "" {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, X-Request-ID")
w.Header().Set("Access-Control-Allow-Credentials", "true")
}
// Se for requisição pre-flight do navegador, responde 204 No Content imediatamente
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
3.5 Timeout de Requisição com http.TimeoutHandler
Um dos maiores riscos em produção é handlers travados por bancos lentos ou APIs externas que seguram conexões e esgotam o pool de goroutines:
func WithTimeout(timeout time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.TimeoutHandler(next, timeout, `{"error":"request_timeout"}`)
}
}
4. O Helper de Encadeamento: Chain()
Encadear middlewares manualmente produz uma sintaxe em cascata difícil de ler:
// ❌ Cascata confusa e contra-intuitiva:
handler = Logger(logger)(Recoverer(logger)(RequestID()(CORS(cfg)(finalHandler))))
Para resolver isso, implementamos uma função auxiliar Chain que executa os middlewares rigorosamente na ordem em que foram declarados:
package middleware
import "net/http"
type Middleware func(http.Handler) http.Handler
// Chain combina múltiplos middlewares em um único pipeline linear
func Chain(middlewares ...Middleware) Middleware {
return func(final http.Handler) http.Handler {
// Itera de trás para frente para que o primeiro middleware seja a casca externa
for i := len(middlewares) - 1; i >= 0; i-- {
final = middlewares[i](final)
}
return final
}
}
Uso no Servidor Principal (main.go)
Integrando perfeitamente com o roteador nativo http.NewServeMux introduzido em Servidor HTTP Nativo Moderno:
package main
import (
"log/slog"
"net/http"
"os"
"time"
"meu-projeto/internal/middleware"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
// Endpoints da API
mux.HandleFunc("GET /api/v1/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"healthy"}`))
})
mux.HandleFunc("POST /api/v1/orders", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
w.Write([]byte(`{"order_id":"12345"}`))
})
// Pipeline de Middlewares Globais
globalMiddleware := middleware.Chain(
middleware.RequestID(),
middleware.Logger(logger),
middleware.Recoverer(logger),
middleware.CORS(middleware.CORSConfig{}),
)
server := &http.Server{
Addr: ":8080",
Handler: globalMiddleware(mux),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
logger.Info("Servidor HTTP rodando na porta :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Error("Erro fatal no servidor", slog.Any("error", err))
}
}
5. Testando Middlewares com net/http/httptest
Middlewares devem ser testados unitariamente de forma isolada, simulando requisições sem precisar subir portas de rede reais:
package middleware_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"meu-projeto/internal/middleware"
)
func TestRequestIDMiddleware(t *testing.T) {
// Handler dummy que verifica se o context foi devidamente populado
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := middleware.GetRequestID(r.Context())
assert.NotEmpty(t, reqID, "o context deve conter um request_id")
w.WriteHeader(http.StatusOK)
})
pipeline := middleware.RequestID()(handler)
// Simula requisição HTTP
req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
pipeline.ServeHTTP(rec, req)
// Validações
require.Equal(t, http.StatusOK, rec.Code)
assert.NotEmpty(t, rec.Header().Get("X-Request-ID"), "o header de resposta deve conter o X-Request-ID")
}
6. Armadilhas Comuns em Middlewares
- A Ordem das Camadas Importa:
O middleware de
Recovererdeve vir depois doLoggerna cadeia. Se oLoggerestiver dentro doRecoverer, um pânico derrubará a requisição antes que oLoggerconsiga calcular o tempo ou emitir o log de saída. - Escrever Headers Múltiplas Vezes:
No Go, o primeiro
w.WriteHeader()fixa o status code e envia os headers HTTP pelo socket. Qualquer chamada subsequente aw.WriteHeader()é ignorada e emite um alerta nos logs (http: superfluous response.WriteHeader call). Use a flag booleanawroteHeaderem seu wrapper. - Chaves de Contexto Não Tipadas:
Nunca use
context.WithValue(ctx, "id", val). O uso de strings primitivas como chave gera conflitos acidentais quando bibliotecas de terceiros usam o mesmo identificador. Declare sempretype ctxKey struct{}outype ctxKey string.
7. Resumo Prático
- A assinatura canônica de um middleware é
func(http.Handler) http.Handler. - O padrão Onion divide a execução em fase de descida (inbound) e fase de subida (outbound).
- Use
wrappedResponseWriterpara espionar o código de status HTTP e a contagem de bytes transmitidos. - Sempre proteja seu servidor de produção com
Recoverere rastreie fluxos comRequestID. - Utilize o helper
Chain()para compor múltiplos middlewares de forma limpa e legível. - Integre os middlewares diretamente ao seu roteador nativo ou arquitetura em Clean Architecture em Go e proteja rotas autenticadas com Autenticação e JWT.