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á


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:

GO
// 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

GO
// 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

GO
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)
}
▶ Experimente

(2) http.Get x http.Client

GO
// 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

GO
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)
}
▶ Experimente

(2) ▶ Exemplo: Controle de tempo limite em níveis

GO
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))
}
▶ Experimente

(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
💡 Dica: O valor padrão de 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

GO 📖 Somente leitura
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))
}
69 linhas de lógica (limite de 40, somente leitura)
100%
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

GO
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))
}
▶ Experimente
💡 Dica: O 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

GO
// 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)
    }
}
🔥 Erro comum: 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.Get e http.Client? R: Use http.Get para testes rápidos. Em um ambiente de produção, use sempre um http.Client personalizado — defina o Timeout para evitar que as solicitações fiquem pendentes e configure o Transport para controlar os detalhes do pool de conexões e do tempo limite. O http.Get usa o http.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 IdleConnTimeout entre 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 Director para 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. O http.Client pode ser usado de forma simultânea (várias chamadas ao Get ou ao Do não exigem bloqueios adicionais). No entanto, os campos do Transport não devem ser modificados após o primeiro uso. Prática recomendada: crie um Client para cada cluster de serviços a jusante; não crie um novo Client para 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 use req.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


📝 Exercícios

  1. Exercício básico (Dificuldade: ⭐): Escreva um http.Client personalizado com um tempo limite de 5 segundos e um MaxIdleConnsPerHost=20. Use esse Client para enviar uma solicitação GET para https://httpbin.org/delay/3 e verifique se o tempo limite funciona conforme o esperado.

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

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

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%