Pular para o conteúdo

Trilha de Go · parte 23 de 37

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

Tamanho da fonte:
Largura da coluna:

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

Pipeline de Middlewares HTTP em Go (Modelo Cebola)
figura 1 · Pipeline de Middlewares HTTP em Go (Modelo Cebola)

Ordem Cronológica de Execução:

  1. A requisição HTTP chega pela camada mais externa (Logging & Metrics). Um cronômetro de alta precisão é iniciado.
  2. A requisição desce para a camada de resiliência (Panic Recovery), onde um bloco defer recover() é registrado na pilha da goroutine.
  3. A requisição passa pela camada de rastreabilidade (Request ID & Context), que injeta um Correlation ID único no context.Context.
  4. O Handler de Negócio executa a lógica do caso de uso e grava o payload e código de status HTTP.
  5. 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:

Interceptação com wrappedResponseWriter
figura 2 · Interceptação com wrappedResponseWriter

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

  1. A Ordem das Camadas Importa: O middleware de Recoverer deve vir depois do Logger na cadeia. Se o Logger estiver dentro do Recoverer, um pânico derrubará a requisição antes que o Logger consiga calcular o tempo ou emitir o log de saída.
  2. 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 a w.WriteHeader() é ignorada e emite um alerta nos logs (http: superfluous response.WriteHeader call). Use a flag booleana wroteHeader em seu wrapper.
  3. 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 sempre type ctxKey struct{} ou type 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 wrappedResponseWriter para espionar o código de status HTTP e a contagem de bytes transmitidos.
  • Sempre proteja seu servidor de produção com Recoverer e rastreie fluxos com RequestID.
  • 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.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?