---
title: "Middleware em Go: net/http, Chi, Gin e o Padrão Handler"
url: "https://golang.com.br/blog/middleware-go-net-http-chi-gin-producao/"
markdown_url: "https://golang.com.br/blog/middleware-go-net-http-chi-gin-producao.MD"
description: "Middleware em Go: padrão http.Handler, chain com net/http, Chi e Gin, recovery, request ID, logging, CORS, auth e rate limit — com exemplos de produção."
date: "2026-07-22"
author: "Golang Brasil"
---

# Middleware em Go: net/http, Chi, Gin e o Padrão Handler

Middleware em Go: padrão http.Handler, chain com net/http, Chi e Gin, recovery, request ID, logging, CORS, auth e rate limit — com exemplos de produção.


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](https://github.com/go-chi/chi) e [Gin](https://github.com/gin-gonic/gin), além dos middlewares que quase toda API no Brasil precisa em 2026. Ele complementa o [comparativo de frameworks HTTP](/blog/frameworks-http-go-gin-echo-fiber-chi/), o [guia de rate limiting](/blog/rate-limiting-go-api-producao/), o [guia de autenticação e autorização](/blog/autenticacao-autorizacao-go-apis/), o [logging com slog](/blog/slog-go-logging-estruturado/), o [context com timeout](/blog/context-timeout-cancelamento-go/) e o [tutorial de API REST](/aprenda/api-rest-go/).

## 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:

```go
type Middleware func(http.Handler) http.Handler
```

O handler final continua sendo o que você já conhece:

```go
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](/blog/clean-architecture-go-sem-overengineering/): interfaces pequenas, composição explícita.

## O padrão canônico: wrapping

O middleware mais simples possível só loga e repassa:

```go
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:

```go
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:

```go
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:

```go
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:

```go
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](/blog/slog-go-logging-estruturado/) e, se já tiver, com o [request ID](#request-id-correlação-de-logs) 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:

```go
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](/blog/go-opentelemetry-observabilidade-tracing-metricas/) e com o [guia de context](/blog/context-timeout-cancelamento-go/): 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:

```go
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](/blog/health-checks-go-liveness-readiness-startup/) 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

```go
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](/blog/autenticacao-autorizacao-go-apis/) e no [tutorial de JWT](/aprenda/golang-jwt-autenticacao/):

```go
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](/blog/rate-limiting-go-api-producao/). 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](https://github.com/go-chi/chi) é 100% `http.Handler`. Por isso o middleware que você escreveu acima funciona sem adaptador:

```go
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()`:

```go
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](/blog/frameworks-http-go-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:

```go
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](/blog/graceful-shutdown-go-producao/). 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

1. **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 chame `next` ou escreva a response de erro e `return`.
2. **Mutar `*http.Request` sem `WithContext`**: o request é compartilhado; o idiomático é `r = r.WithContext(ctx)` e passar adiante.
3. **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](/blog/upload-arquivos-go-multipart-s3-seguranca/).
4. **Middleware global demais**: auth global em `/healthz` e `/metrics` quebra probes do Kubernetes. Use grupos (Chi) ou `r.Group` (Gin).
5. **Depender de Fiber middleware em biblioteca interna**: se um dia o time migrar para Chi/`net/http`, você reescreve tudo. Prefira `func(http.Handler) http.Handler` em libs compartilhadas.
6. **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`:

```go
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](/blog/mocks-go-testify-gomock-fakes-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](/blog/dependency-injection-go-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](/blog/autenticacao-autorizacao-go-apis/).

### 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](/blog/frameworks-http-go-gin-echo-fiber-chi/) 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](/blog/frameworks-http-go-gin-echo-fiber-chi/)
- [Rate limiting em APIs Go](/blog/rate-limiting-go-api-producao/)
- [Autenticação e autorização](/blog/autenticacao-autorizacao-go-apis/)
- [slog: logging estruturado](/blog/slog-go-logging-estruturado/)
- [Context, timeout e cancelamento](/blog/context-timeout-cancelamento-go/)
- [Graceful shutdown](/blog/graceful-shutdown-go-producao/)
- [Health checks](/blog/health-checks-go-liveness-readiness-startup/)
- [API REST com Go](/aprenda/api-rest-go/)
- [Vagas de Go no Brasil](/vagas/)

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.
