Golang

Artigo 30 — Consumindo APIs externas com http.Client Já leu

11 min de leitura

Artigo 30 — Consumindo APIs externas com http.Client
Artigo 30 — Consumindo APIs externas com http.Client O cliente que já vem configurado — mas não do jeito certo Go oferece um cliente HTTP completo na

Artigo 30 — Consumindo APIs externas com http.Client

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 5 — I/O, HTTP e APIs


O cliente que já vem configurado — mas não do jeito certo

Go oferece um cliente HTTP completo na biblioteca padrão. A tentação é usar http.Get diretamente — uma função de conveniência que funciona, mas esconde um problema sério: ela usa o cliente padrão global, que não tem timeout configurado. Em produção, uma API externa que nunca responde pode travar uma goroutine indefinidamente, acumulando conexões abertas até degradar o serviço inteiro.

A regra é simples: nunca use http.Get, http.Post ou http.DefaultClient em código de produção. Sempre crie um http.Client com timeout explícito.


Criando um cliente HTTP correto

package main

import (
    "net/http"
    "time"
)

func novoClienteHTTP() *http.Client {
    return &http.Client{
        Timeout: 30 * time.Second, // timeout total da requisição

        Transport: &http.Transport{
            MaxIdleConns:        100,
            MaxIdleConnsPerHost: 10,
            IdleConnTimeout:     90 * time.Second,
            TLSHandshakeTimeout: 10 * time.Second,
            ResponseHeaderTimeout: 10 * time.Second,
            DisableCompression:  false,
        },
    }
}

O Timeout cobre toda a requisição — desde o dial TCP até a leitura completa do corpo. O Transport controla o pool de conexões, que é reutilizado entre requisições para evitar o custo de criar novas conexões TCP a cada chamada.


GET simples com tratamento completo

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

type ViaCEP struct {
    CEP        string `json:"cep"`
    Logradouro string `json:"logradouro"`
    Bairro     string `json:"bairro"`
    Localidade string `json:"localidade"`
    UF         string `json:"uf"`
    Erro       bool   `json:"erro"`
}

var cliente = &http.Client{Timeout: 10 * time.Second}

func buscarCEP(cep string) (*ViaCEP, error) {
    url := fmt.Sprintf("https://viacep.com.br/ws/%s/json/", cep)

    resp, err := cliente.Get(url)
    if err != nil {
        return nil, fmt.Errorf("buscarCEP: requisição falhou: %w", err)
    }
    defer resp.Body.Close()

    // Sempre verificar o status HTTP
    if resp.StatusCode != http.StatusOK {
        corpo, _ := io.ReadAll(resp.Body)
        return nil, fmt.Errorf("buscarCEP: status %d: %s",
            resp.StatusCode, string(corpo))
    }

    var endereco ViaCEP
    if err := json.NewDecoder(resp.Body).Decode(&endereco); err != nil {
        return nil, fmt.Errorf("buscarCEP: decodificar resposta: %w", err)
    }

    if endereco.Erro {
        return nil, fmt.Errorf("buscarCEP: CEP %s não encontrado", cep)
    }

    return &endereco, nil
}

func main() {
    endereco, err := buscarCEP("01310100")
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }

    fmt.Printf("%s, %s — %s/%s\n",
        endereco.Logradouro,
        endereco.Bairro,
        endereco.Localidade,
        endereco.UF,
    )
}

Construindo requisições com http.NewRequest

Para controle total sobre a requisição — headers, body, context — use http.NewRequest:

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

type NovoPedido struct {
    ProdutoID int     `json:"produto_id"`
    Quantidade int    `json:"quantidade"`
    Observacao string `json:"observacao,omitempty"`
}

type RespostaPedido struct {
    ID     int    `json:"id"`
    Status string `json:"status"`
}

func criarPedido(ctx context.Context, baseURL, token string, pedido NovoPedido) (*RespostaPedido, error) {
    // Serializa o corpo
    corpo, err := json.Marshal(pedido)
    if err != nil {
        return nil, fmt.Errorf("serializar pedido: %w", err)
    }

    // Cria a requisição com context para cancelamento
    req, err := http.NewRequestWithContext(
        ctx,
        http.MethodPost,
        baseURL+"/pedidos",
        bytes.NewReader(corpo),
    )
    if err != nil {
        return nil, fmt.Errorf("criar requisição: %w", err)
    }

    // Headers
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer "+token)
    req.Header.Set("Accept", "application/json")
    req.Header.Set("X-Request-ID", "req-abc-123")

    cliente := &http.Client{Timeout: 15 * time.Second}
    resp, err := cliente.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executar requisição: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusCreated {
        return nil, fmt.Errorf("status inesperado: %d", resp.StatusCode)
    }

    var resposta RespostaPedido
    if err := json.NewDecoder(resp.Body).Decode(&resposta); err != nil {
        return nil, fmt.Errorf("decodificar resposta: %w", err)
    }

    return &resposta, nil
}

func main() {
    ctx, cancelar := context.WithTimeout(context.Background(), 20*time.Second)
    defer cancelar()

    pedido := NovoPedido{ProdutoID: 42, Quantidade: 3}
    resp, err := criarPedido(ctx, "https://api.exemplo.com", "meu-token", pedido)
    if err != nil {
        fmt.Println("Erro:", err)
        return
    }

    fmt.Printf("Pedido criado: ID=%d Status=%s\n", resp.ID, resp.Status)
}

Cliente HTTP reutilizável com configuração

Para projetos reais, encapsule o cliente em uma struct com configurações e métodos auxiliares:

package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "time"
)

type ClienteAPI struct {
    baseURL    string
    token      string
    httpClient *http.Client
}

func NovoClienteAPI(baseURL, token string) *ClienteAPI {
    return &ClienteAPI{
        baseURL: baseURL,
        token:   token,
        httpClient: &http.Client{
            Timeout: 30 * time.Second,
            Transport: &http.Transport{
                MaxIdleConnsPerHost: 10,
                IdleConnTimeout:     90 * time.Second,
            },
        },
    }
}

func (c *ClienteAPI) fazer(ctx context.Context, metodo, caminho string, corpo any) (*http.Response, error) {
    var bodyReader io.Reader

    if corpo != nil {
        dados, err := json.Marshal(corpo)
        if err != nil {
            return nil, fmt.Errorf("serializar corpo: %w", err)
        }
        bodyReader = bytes.NewReader(dados)
    }

    req, err := http.NewRequestWithContext(ctx, metodo, c.baseURL+caminho, bodyReader)
    if err != nil {
        return nil, fmt.Errorf("criar requisição: %w", err)
    }

    if corpo != nil {
        req.Header.Set("Content-Type", "application/json")
    }
    req.Header.Set("Authorization", "Bearer "+c.token)
    req.Header.Set("Accept", "application/json")

    resp, err := c.httpClient.Do(req)
    if err != nil {
        return nil, fmt.Errorf("executar %s %s: %w", metodo, caminho, err)
    }

    return resp, nil
}

func (c *ClienteAPI) decodificar(resp *http.Response, destino any) error {
    defer resp.Body.Close()

    if resp.StatusCode >= 400 {
        corpo, _ := io.ReadAll(resp.Body)
        return fmt.Errorf("erro HTTP %d: %s", resp.StatusCode, string(corpo))
    }

    if destino != nil && resp.StatusCode != http.StatusNoContent {
        if err := json.NewDecoder(resp.Body).Decode(destino); err != nil {
            return fmt.Errorf("decodificar resposta: %w", err)
        }
    }

    return nil
}

// Métodos de alto nível
func (c *ClienteAPI) Get(ctx context.Context, caminho string, resultado any) error {
    resp, err := c.fazer(ctx, http.MethodGet, caminho, nil)
    if err != nil {
        return err
    }
    return c.decodificar(resp, resultado)
}

func (c *ClienteAPI) Post(ctx context.Context, caminho string, corpo, resultado any) error {
    resp, err := c.fazer(ctx, http.MethodPost, caminho, corpo)
    if err != nil {
        return err
    }
    return c.decodificar(resp, resultado)
}

func (c *ClienteAPI) Put(ctx context.Context, caminho string, corpo, resultado any) error {
    resp, err := c.fazer(ctx, http.MethodPut, caminho, corpo)
    if err != nil {
        return err
    }
    return c.decodificar(resp, resultado)
}

func (c *ClienteAPI) Delete(ctx context.Context, caminho string) error {
    resp, err := c.fazer(ctx, http.MethodDelete, caminho, nil)
    if err != nil {
        return err
    }
    return c.decodificar(resp, nil)
}

// Uso do cliente
type Usuario struct {
    ID    int    `json:"id"`
    Nome  string `json:"nome"`
    Email string `json:"email"`
}

func main() {
    cliente := NovoClienteAPI("https://jsonplaceholder.typicode.com", "token-exemplo")
    ctx := context.Background()

    // GET com query parameters
    params := url.Values{}
    params.Set("_limit", "3")

    var usuarios []Usuario
    if err := cliente.Get(ctx, "/users?"+params.Encode(), &usuarios); err != nil {
        fmt.Println("Erro:", err)
        return
    }

    for _, u := range usuarios {
        fmt.Printf("[%d] %s <%s>\n", u.ID, u.Nome, u.Email)
    }
}

Requisições concorrentes com controle de rate

Para buscar múltiplos recursos em paralelo com limite de concorrência:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "sync"
    "time"
)

type Post struct {
    ID     int    `json:"id"`
    Titulo string `json:"title"`
    Corpo  string `json:"body"`
}

type ResultadoBusca struct {
    ID   int
    Post *Post
    Erro error
}

func buscarPostsConcorrente(ctx context.Context, ids []int, maxConcorrente int) []ResultadoBusca {
    semaforo := make(chan struct{}, maxConcorrente)
    resultados := make([]ResultadoBusca, len(ids))

    cliente := &http.Client{Timeout: 10 * time.Second}

    var wg sync.WaitGroup
    for i, id := range ids {
        wg.Add(1)
        i, id := i, id
        go func() {
            defer wg.Done()

            // Adquire slot no semáforo
            select {
            case semaforo <- struct{}{}:
                defer func() { <-semaforo }()
            case <-ctx.Done():
                resultados[i] = ResultadoBusca{ID: id, Erro: ctx.Err()}
                return
            }

            url := fmt.Sprintf("https://jsonplaceholder.typicode.com/posts/%d", id)
            req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
            if err != nil {
                resultados[i] = ResultadoBusca{ID: id, Erro: err}
                return
            }

            resp, err := cliente.Do(req)
            if err != nil {
                resultados[i] = ResultadoBusca{ID: id, Erro: err}
                return
            }
            defer resp.Body.Close()

            var post Post
            if err := json.NewDecoder(resp.Body).Decode(&post); err != nil {
                resultados[i] = ResultadoBusca{ID: id, Erro: err}
                return
            }

            resultados[i] = ResultadoBusca{ID: id, Post: &post}
        }()
    }

    wg.Wait()
    return resultados
}

func main() {
    ctx, cancelar := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancelar()

    ids := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    inicio := time.Now()

    resultados := buscarPostsConcorrente(ctx, ids, 3) // máximo 3 simultâneas

    for _, r := range resultados {
        if r.Erro != nil {
            fmt.Printf("[%d] ERRO: %v\n", r.ID, r.Erro)
        } else {
            titulo := r.Post.Titulo
            if len(titulo) > 40 {
                titulo = titulo[:40] + "..."
            }
            fmt.Printf("[%d] %s\n", r.ID, titulo)
        }
    }

    fmt.Printf("\n%d posts em %v\n", len(ids), time.Since(inicio))
}

Retry com backoff exponencial

APIs externas falham. Um cliente robusto deve tentar novamente com intervalos crescentes:

package main

import (
    "context"
    "fmt"
    "math/rand/v2"
    "net/http"
    "time"
)

type ConfigRetry struct {
    MaxTentativas int
    EsperaInicial time.Duration
    EsperaMaxima  time.Duration
    Multiplicador float64
}

var ConfigRetryPadrao = ConfigRetry{
    MaxTentativas: 3,
    EsperaInicial: 100 * time.Millisecond,
    EsperaMaxima:  5 * time.Second,
    Multiplicador: 2.0,
}

func comRetry(ctx context.Context, config ConfigRetry, fn func() (*http.Response, error)) (*http.Response, error) {
    espera := config.EsperaInicial

    for tentativa := 1; tentativa <= config.MaxTentativas; tentativa++ {
        resp, err := fn()

        // Sucesso
        if err == nil && resp.StatusCode < 500 {
            return resp, nil
        }

        // Última tentativa — retorna o erro
        if tentativa == config.MaxTentativas {
            if err != nil {
                return nil, fmt.Errorf("após %d tentativas: %w", tentativa, err)
            }
            return resp, nil
        }

        // Fecha o corpo antes de tentar novamente
        if resp != nil {
            resp.Body.Close()
        }

        // Calcula espera com jitter para evitar thundering herd
        jitter := time.Duration(rand.Float64() * float64(espera) * 0.1)
        esperaComJitter := espera + jitter

        fmt.Printf("tentativa %d falhou — aguardando %v\n", tentativa, esperaComJitter)

        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(esperaComJitter):
        }

        // Backoff exponencial
        espera = time.Duration(float64(espera) * config.Multiplicador)
        if espera > config.EsperaMaxima {
            espera = config.EsperaMaxima
        }
    }

    return nil, fmt.Errorf("todas as tentativas esgotadas")
}

func main() {
    ctx, cancelar := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancelar()

    cliente := &http.Client{Timeout: 5 * time.Second}

    resp, err := comRetry(ctx, ConfigRetryPadrao, func() (*http.Response, error) {
        return cliente.Get("https://httpbin.org/status/200")
    })

    if err != nil {
        fmt.Println("Erro:", err)
        return
    }
    defer resp.Body.Close()

    fmt.Println("Sucesso! Status:", resp.Status)
}

Testando código que consome APIs externas

Para testar sem depender de APIs reais, use httptest.NewServer:

package main

import (
    "context"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestBuscarCEP(t *testing.T) {
    // Servidor falso que simula a API do ViaCEP
    servidor := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path == "/ws/01310100/json/" {
            json.NewEncoder(w).Encode(ViaCEP{
                CEP:        "01310-100",
                Logradouro: "Avenida Paulista",
                Bairro:     "Bela Vista",
                Localidade: "São Paulo",
                UF:         "SP",
            })
            return
        }
        http.NotFound(w, r)
    }))
    defer servidor.Close()

    // Substitui a URL real pela URL do servidor de teste
    urlOriginal := "https://viacep.com.br"
    _ = urlOriginal

    ctx := context.Background()
    _ = ctx

    // Verifica o handler diretamente
    req := httptest.NewRequest(http.MethodGet, "/ws/01310100/json/", nil)
    rr := httptest.NewRecorder()
    servidor.Config.Handler.ServeHTTP(rr, req)

    if rr.Code != http.StatusOK {
        t.Errorf("status esperado 200, obtido %d", rr.Code)
    }

    var resultado ViaCEP
    json.NewDecoder(rr.Body).Decode(&resultado)

    if resultado.Localidade != "São Paulo" {
        t.Errorf("cidade esperada 'São Paulo', obtida '%s'", resultado.Localidade)
    }
}

Resumo do que foi coberto

Este artigo apresentou o consumo de APIs externas em Go: a importância de sempre configurar timeout no cliente HTTP, construção de requisições com http.NewRequestWithContext, um cliente HTTP reutilizável com métodos de alto nível, requisições concorrentes com semáforo para controle de rate, retry com backoff exponencial e jitter, e testes com httptest.NewServer para simular APIs externas. O próximo artigo explora middlewares com foco em logging, autenticação e CORS.


Referências e leituras complementares

  • Documentação do pacote net/http — Client — Referência completa do cliente HTTP. https://pkg.go.dev/net/http#Client

  • Documentação do pacote net/http — Transport — Configuração do pool de conexões. https://pkg.go.dev/net/http#Transport

  • Go by Example: HTTP Clients — Exemplos práticos de requisições HTTP. https://gobyexample.com/http-clients

  • API ViaCEP — API pública brasileira de CEPs usada nos exemplos. https://viacep.com.br

  • httpbin.org — Serviço para testar clientes HTTP com respostas configuráveis. https://httpbin.org

  • resty — Biblioteca popular para clientes HTTP com interface fluente. https://github.com/go-resty/resty


Próximo artigo: Artigo 31 — Middlewares: logging, autenticação e CORS**


you asked

Sim


claude response

Comentários

Mais em Golang

Maps: criação, iteração e boas práticas
Maps: criação, iteração e boas práticas

Se slices s&atilde;o a espinha dorsal das sequ&ecirc;ncias em Go, maps s&atil...

GORM: ORM para Go, migrações e relacionamentos
GORM: ORM para Go, migrações e relacionamentos

ORM — Object-Relational Mapper — é uma camada que mapeia structs Go para tabe...

sync.WaitGroup e sync.Mutex: coordenando goroutines
sync.WaitGroup e sync.Mutex: coordenando goroutines

Channels são elegantes para comunicação entre goroutines, mas nem toda situaç...