Go: Clientes e middleware Go HTTP
Última atualização: 2026-08-26
Os clientes HTTP e o middleware são os dois pilares da comunicação entre microsserviços — um gerencia as conexões e o outro lida com questões transversais.
Quando seu serviço precisa chamar outros cinco serviços a jusante, cada chamada exige registro em log, gerenciamento de tempo limite, novas tentativas e interrupção de circuito — você deve escrever esse código cinco vezes ou abstraí-lo uma única vez? Nesta lição, você vai dominar o uso avançado do cliente HTTP do Go e dos padrões de design de middleware.
1. Você aprenderá
http.Get/Post/DoSolicitações básicashttp.ClientControle de tempo limitehttp.Transportconfiguração do pool de conexões- Padrão de middleware (Modelo Cebola)
- Middleware comum: registro de logs, recuperação, CORS, tempos limite
httputil.ReverseProxyproxy reverso
2. A história real de um engenheiro de backend
(1) Desafio: Cinco serviços a jusante, cada um exigindo registro em log, gerenciamento de tempo limite e novas tentativas
Charlie é responsável pelo serviço de gateway de API da empresa, que precisa chamar cinco serviços a jusante:
“Toda chamada a jusante deve: gerar um registro de solicitação (para facilitar o diagnóstico de problemas), definir um tempo limite (para evitar impasses) e registrar a duração (para monitoramento e alertas). Acabei escrevendo o mesmo código cinco vezes — uma função por serviço, cada uma contendo a mesma sequência repetitiva de registro, tempo limite e a própria chamada.”
Seu código na época:
// Bad code: every downstream service repeats the same logic
func callUserService(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] Requesting user service: %s", r.Method, r.URL.Path)
start := time.Now()
resp, err := http.Get("http://user-service/api/users")
log.Printf("[%s] User service elapsed: %v", r.Method, time.Since(start))
// ... process response
}
func callOrderService(w http.ResponseWriter, r *http.Request) {
log.Printf("[%s] Requesting order service: %s", r.Method, r.URL.Path)
start := time.Now()
resp, err := http.Get("http://order-service/api/orders")
log.Printf("[%s] Order service elapsed: %v", r.Method, time.Since(start))
// ... same pattern again!
}
// Every new service repeats!
(2) Solução em Go: Padrão de middleware + Cliente personalizado
// Middleware: wraps http.RoundTripper
type LoggingMiddleware struct {
next http.RoundTripper
}
func (m *LoggingMiddleware) RoundTrip(req *http.Request) (*http.Response, error) {
start := time.Now()
log.Printf("[%s] %s %s", req.Method, req.URL.Host, req.URL.Path)
resp, err := m.next.RoundTrip(req)
log.Printf("[%s] %s elapsed: %v", req.Method, req.URL.Path, time.Since(start))
return resp, err
}
// Unified Client: shared by all downstream services
client := &http.Client{
Timeout: 5 * time.Second,
Transport: &LoggingMiddleware{
next: http.DefaultTransport,
},
}
// All downstream calls automatically get logging + timeout
resp1, _ := client.Get("http://user-service/api/users") // Auto logging
resp2, _ := client.Get("http://Ordem-service/api/orders") // Auto logging
(3) Benefícios: antes e depois da implementação do middleware
| Dimensão | Código duplicado | Padrão de middleware |
|---|---|---|
| Volume de código | 30 linhas por downstream, 5 downstreams = 150 linhas | 15 linhas de middleware, totalizando 30 linhas |
| Adicionar a jusante | Copiar e colar 30 linhas | Chamar client.Get diretamente |
| Tempo limite de edição | Alterar 5 locais | Alterar 1 cliente.Tempo limite |
3. Noções básicas sobre clientes HTTP
(1) ▶ Exemplo: GET / POST / Solicitações personalizadas
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
func main() {
// 1. GET request
resp, err := http.Get("https://api.example.com/users")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("GET status: %d\n", resp.StatusCode)
fmt.Printf("Response: %s\n", body)
// 2. POST JSON request
data := map[string]string{"name": "Alice"}
jsonData, _ := json.Marshal(data)
resp, err = http.Post(
"https://api.example.com/users",
"application/json",
bytes.NewReader(jsonData),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
// 3. Custom request (set Headers)
req, _ := http.NewRequest("DELETE", "https://api.example.com/users/1", nil)
req.Header.Set("Authorization", "Bearer token-123")
req.Header.Set("X-Request-ID", "req-456")
resp, err = http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
fmt.Printf("DELETE status: %d\n", resp.StatusCode)
}
(2) http.Get x http.Client
// Method 1: Use http.Get directly (default Client)
resp, _ := http.Get(url)
// Problem: No default timeout, may hang forever!
// Method 2: Custom Client (recommended)
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, _ := client.Get(url)
// Safe: 10-second timeout auto-cancels
| Método | Tempo limite | Pool de conexões | Recomendação |
|---|---|---|---|
http.Get(url) |
Sem tempo limite | Padrão (reutilização limitada) | ❌ Apenas para testes rápidos |
http.DefaultClient |
Sem tempo limite | Padrão | ⚠️ Use com cautela em ambiente de produção |
&http.Client{Timeout: 10s} |
Tempo limite global | Padrão | ✅ Recomendado |
&http.Client{Timeout, Transport} |
Tempo limite global | Pool de conexões personalizado | ✅ Pronto para produção |
4. Limites de tempo do http.Client e transporte
(1) ▶ Exemplo: Configurando tempos limite e conjuntos de conexões
package main
import (
"fmt"
"net"
"net/http"
"time"
)
func main() {
// Custom Transport
transport := &http.Transport{
// Connection pool
MaxIdleConns: 100, // Max idle connections
MaxIdleConnsPerHost: 10, // Max idle connections per host
IdleConnTimeout: 90 * time.Second, // Idle connection timeout
// TLS
TLSHandshakeTimeout: 10 * time.Second,
// Dial
DialContext: (&net.Dialer{
Timeout: 30 * time.Second, // Connection timeout
KeepAlive: 30 * time.Second, // Keep-Alive interval
}).DialContext,
}
// Custom Client
client := &http.Client{
Timeout: 30 * time.Second, // Total request timeout (includes all stages)
Transport: transport,
}
// Use client to send request
resp, err := client.Get("https://api.example.com/users")
if err != nil {
fmt.Printf("Request failed: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Status: %d\n", resp.StatusCode)
}
(2) ▶ Exemplo: Controle de tempo limite em níveis
package main
import (
"fmt"
"net"
"net/http"
"time"
)
func main() {
// Timeout tiers:
// Transport.DialContext → Connection timeout (10s)
// Transport.TLSHandshake → TLS handshake timeout (5s)
// Transport.ResponseHeader → Response header timeout (10s)
// Client.Timeout → Total timeout (30s, covers Dial + TLS + send + read)
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
client := &http.Client{
Timeout: 30 * time.Second,
Transport: transport,
}
start := time.Now()
resp, err := client.Get("https://httpbin.org/delay/5")
if err != nil {
fmt.Printf("Error: %v (elapsed: %v)\n", err, time.Since(start))
return
}
defer resp.Body.Close()
fmt.Printf("Success: %d (elapsed: %v)\n", resp.StatusCode, time.Since(start))
}
(3) Parâmetros de configuração de transporte
| Parâmetro | Padrão | Recomendado | Descrição |
|---|---|---|---|
MaxIdleConns |
100 | 100–200 | Valor máximo global de conexões inativas |
MaxIdleConnsPerHost |
2 | 10–50 | Valor máximo de conexões inativas por host (o padrão de 2 é muito baixo!) |
IdleConnTimeout |
anos 90 | 30–90s | Tempo limite de conexão inativa |
TLSHandshakeTimeout |
10 s | 5–10 s | Tempo limite do handshake TLS |
ResponseHeaderTimeout |
0 (Nunca) | 10–30s | Tempo limite do cabeçalho de resposta |
DialContext.Timeout |
Nenhuma | 10–30 s | Tempo limite da conexão TCP |
MaxIdleConnsPerHost em http.Transport é de apenas 2, o que é extremamente insuficiente para cenários que envolvem chamadas com alta simultaneidade para o mesmo serviço. Certifique-se de aumentar esse valor de acordo com o nível de simultaneidade (por exemplo, 50–100).
5. Padrão de middleware
(1) ▶ Exemplo: O modelo “Onion” de middleware
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
// Middleware type: receives Handler, returns Handler
type Middleware func(http.Handler) http.Handler
// Middleware chain: assembles all middleware into one
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// ---------- Concrete middleware ----------
// 1. Logging middleware
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
log.Printf("[%s] %s elapsed: %v", r.Method, r.URL.Path, time.Since(start))
})
}
// 2. Recovery middleware (prevents panic from crashing the service)
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// 3. CORS middleware
func CORSMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// 4. Timeout middleware
func TimeoutMiddleware(timeout time.Duration) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), timeout)
defer cancel()
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// ---------- Usage ----------
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello", helloHandler)
// Chain all middleware (onion model: Recovery → Logging → CORS → Handler)
handler := Chain(mux,
RecoveryMiddleware,
LoggingMiddleware,
CORSMiddleware,
TimeoutMiddleware(5*time.Second),
)
log.Print(http.ListenAndServe(":8080", handler))
}
graph LR
Req[Request] --> R[Recovery]
R --> L[Logging]
L --> C[CORS]
C --> T[Timeout]
T --> H[Handler]
H -->|Response| T
T --> C
C --> L
L --> R
R --> Resp[Response]
(2) As responsabilidades dos quatro componentes principais do middleware
| Middleware | Responsabilidades | Ordem |
|---|---|---|
| Recovery | Catch panic, return 500 | Outermost |
| Registro | Tempos de solicitação e resposta de registro | Camada 2 |
| CORS | Tratamento de cabeçalhos de solicitações entre origens | Camada 3 |
| Tempo limite | Controle de tempo limite da solicitação | Camada mais interna (próxima ao manipulador) |
6. httputil.ReverseProxy – Proxy reverso
(1) ▶ Exemplo: Proxy reverso
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
func main() {
// Target service address
target, _ := url.Parse("http://localhost:8081")
// Create reverse proxy
proxy := httputil.NewSingleHostReverseProxy(target)
// Custom Director (modify request headers)
proxy.Director = func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = target.Path + req.URL.Path
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
req.Header.Set("X-Real-IP", req.RemoteAddr)
}
// Custom error handling
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Proxy error: %v", err)
http.Error(w, "Bad Gateway", http.StatusBadGateway)
}
mux := http.NewServeMux()
mux.HandleFunc("/api/", proxy.ServeHTTP)
log.Print("API gateway started on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
httputil.ReverseProxy lida automaticamente com a maioria dos detalhes do proxy: encaminhamento do cabeçalho Host, X-Forwarded-For, passagem de cabeçalhos de resposta e reutilização do pool de conexões. Tudo o que você precisa fazer é fornecer uma função Director para modificar a solicitação.
7. Exemplo completo: API Gateway + Pipeline de middleware
// api_gateway.go
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"strings"
"time"
)
// ---------- Configuration ----------
type Route struct {
Path string
Target *url.URL
}
type GatewayConfig struct {
Port string
Routes []Route
Middlewares []Middleware
}
// ---------- Middleware ----------
type Middleware func(http.Handler) http.Handler
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
// Logging
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("[%s] %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("[%s] %s → %v", r.Method, r.URL.Path, time.Since(start))
})
}
// Recovery
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("[PANIC] %v", err)
http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// CORS
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// Rate limiting (simplified token bucket)
type RateLimiter struct {
tokens chan struct{}
}
func NewRateLimiter(rate int) *RateLimiter {
rl := &RateLimiter{tokens: make(chan struct{}, rate)}
for i := 0; i < rate; i++ {
rl.tokens <- struct{}{}
}
// Refill every second
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
select {
case rl.tokens <- struct{}{}:
default:
}
}
}()
return rl
}
func (rl *RateLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case <-rl.tokens:
next.ServeHTTP(w, r)
default:
http.Error(w, `{"error":"rate limit exceeded"}`, http.StatusTooManyRequests)
}
})
}
// ---------- Gateway ----------
type Gateway struct {
config GatewayConfig
proxy *httputil.ReverseProxy
}
func NewGateway(config GatewayConfig) *Gateway {
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
// Match route based on request path
for _, route := range config.Routes {
if strings.HasPrefix(req.URL.Path, route.Path) {
req.URL.Scheme = route.Target.Scheme
req.URL.Host = route.Target.Host
req.URL.Path = strings.TrimPrefix(req.URL.Path, route.Path)
req.Header.Set("X-Forwarded-Host", req.Header.Get("Host"))
return
}
}
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("Proxy error: %v", err)
http.Error(w, `{"error":"bad gateway"}`, http.StatusBadGateway)
},
}
return &Gateway{config: config, proxy: proxy}
}
func (g *Gateway) Start() error {
routesJSON, _ := json.MarshalIndent(g.config.Routes, "", " ")
log.Printf("Loading routes:\n%s\n", routesJSON)
// Route distribution
mux := http.NewServeMux()
for _, route := range g.config.Routes {
pattern := route.Path
if !strings.HasSuffix(pattern, "/") {
pattern += "/"
}
mux.Handle(pattern, g.proxy)
}
// Health check
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// Middleware chain
handler := Chain(mux, g.config.Middlewares...)
server := &http.Server{
Addr: ":" + g.config.Port,
Handler: handler,
}
// Graceful shutdown
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt)
<-sigCh
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
server.Shutdown(ctx)
}()
log.Printf("API gateway started on :%s", g.config.Port)
return server.ListenAndServe()
}
func main() {
userService, _ := url.Parse("http://localhost:8081")
orderService, _ := url.Parse("http://localhost:8082")
config := GatewayConfig{
Port: "8080",
Routes: []Route{
{Path: "/api/users", Target: userService},
{Path: "/api/orders", Target: orderService},
},
}
config.Middlewares = []Middleware{
Recovery,
Logging,
CORS,
}
gateway := NewGateway(config)
if err := gateway.Start(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}
httputil.ReverseProxy modifica o cabeçalho X-Forwarded-For por padrão. Se você não quiser esse comportamento, remova o cabeçalho em Director. Além disso, ReverseProxy não lida automaticamente com atualizações de WebSocket — o método ServeHTTP de httputil.ReverseProxy não oferece suporte a WebSockets, portanto, você deve lidar com a solicitação de atualização separadamente.
❓ Perguntas Frequentes
P: Como faço para escolher entre
http.Getehttp.Client? R: Usehttp.Getpara testes rápidos. Em um ambiente de produção, use sempre umhttp.Clientpersonalizado — defina oTimeoutpara evitar que as solicitações fiquem pendentes e configure oTransportpara controlar os detalhes do pool de conexões e do tempo limite. Ohttp.Getusa ohttp.DefaultClient, que não possui tempo limite padrão.
P: Como configuro o pool de conexões de transporte? R: Os parâmetros principais são MaxIdleConns e MaxIdleConnsPerHost. Por padrão, MaxIdleConnsPerHost está definido como 2, o que é extremamente insuficiente para serviços com alta simultaneidade. Recomenda-se defini-lo entre 50 e 100 (dependendo da simultaneidade). Defina
IdleConnTimeoutentre 30 e 90 segundos para manter as conexões ativas.
P: Qual é a ordem de execução do middleware? R: O modelo em camadas — as solicitações entram pela camada mais externa e percorrem cada camada até o Handler; as respostas retornam do Handler, passando por cada camada até a camada mais externa. A ordem de registro determina a ordem de execução:
Chain(h, A, B, C)→ As solicitações passam por A → B → C → Handler; as respostas passam por C → B → A.
P: Para quais cenários o httputil.ReverseProxy é adequado? R: Gateways de API, proxies reversos e roteamento de serviços. Ele lida automaticamente com o cabeçalho Host, o cabeçalho X-Forwarded-For e a reutilização do pool de conexões. Você só precisa implementar a função
Directorpara modificar o Scheme, o Host e o Path da solicitação. É adequado como ponto de entrada de tráfego para arquiteturas de microsserviços.
P: Como faço para implementar novas tentativas de solicitações? R: Implemente um RoundTripper personalizado na camada de transporte ou encapsule o método Do na camada de cliente. Observe a idempotência das novas tentativas — apenas métodos seguros, como GET, HEAD e OPTIONS, podem ser repetidos automaticamente. Para solicitações POST/PUT, é necessário verificar se o corpo da solicitação pode ser relido antes de tentar novamente.
P: O
http.Clienté seguro para multithreading? R: Sim. Ohttp.Clientpode ser usado de forma simultânea (várias chamadas aoGetou aoDonão exigem bloqueios adicionais). No entanto, os campos doTransportnão devem ser modificados após o primeiro uso. Prática recomendada: crie umClientpara cada cluster de serviços a jusante; não crie um novoClientpara cada solicitação.
P: Como faço para definir um cabeçalho para uma solicitação? R: Após criar uma solicitação usando
http.NewRequest, defina o cabeçalho da seguinte forma:req.Header.Set("Key", "Value"). Não usereq.Header.Add("Key", "Value")—Add, pois eles são anexados em vez de sobrescreverem. Cabeçalhos comuns, como Auth, TraceID e Content-Type, são todos passados nos cabeçalhos da solicitação.
📖 Resumo
- Os três tipos de solicitações: HTTP GET, POST e DO
- Definir um tempo limite para o http.Client a fim de evitar que ele trave
- Configuração do pool de conexões do http.Transport (Aumente o valor de MaxIdleConnsPerHost)
- Padrão de middleware: modelo em camadas, no qual as solicitações percorrem o caminho da camada externa para a camada interna
- 4 componentes principais de middleware: Recuperação → Registro em log → CORS → Tempo limite
- O httputil.ReverseProxy implementa um proxy reverso
- O API Gateway integra a cadeia de middleware + roteamento e distribuição
📝 Exercícios
-
Exercício básico (Dificuldade: ⭐): Escreva um
http.Clientpersonalizado com um tempo limite de 5 segundos e umMaxIdleConnsPerHost=20. Use esseClientpara enviar uma solicitação GET para https://httpbin.org/delay/3 e verifique se o tempo limite funciona conforme o esperado. -
Problema avançado (Dificuldade ⭐⭐): Implemente um middleware de repetição de solicitações (camada RoundTripper). Requisitos: (1) Repita automaticamente as solicitações GET até 3 vezes; (2) Defina os intervalos de repetição para 100 ms, 200 ms e 400 ms, respectivamente (backoff exponencial); (3) Repita apenas as respostas 5xx; não repita as respostas 4xx; (4) Registre cada tentativa de repetição.
-
Desafio (Dificuldade: ⭐⭐⭐): Implemente um proxy reverso com balanceamento de carga. Requisitos: (1) Suportar o registro de múltiplas instâncias de back-end (por exemplo, 3 instâncias do serviço de usuários); (2) Distribuir as solicitações usando o algoritmo Round-Robin; (3) Realize verificações passivas de integridade — remova uma instância após 3 falhas consecutivas e adicione-a novamente assim que ela se recuperar; (4) Registre o endereço de back-end para o qual cada solicitação é roteada; (5) Implemente usando
httputil.ReverseProxy.