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