Cliente HTTP
Nesta aula, você aprenderá a criar clientes HTTP em Go, desde o uso simples de http.Get até a configuração de um http.Client com timeouts e requisições customizadas. Também veremos como tratar respostas de forma eficiente, incluindo o fechamento do corpo e a leitura de dados, com exemplos práticos e exercícios.
Nesta aula, vamos explorar o pacote net/http da biblioteca padrão de Go, que fornece todas as ferramentas necessárias para criar clientes HTTP. Você aprenderá desde a função mais simples http.Get até a criação de um http.Client configurável, com controle de timeouts e requisições personalizadas. O HTTP é a espinha dorsal da comunicação entre sistemas, e dominar o lado do cliente é essencial para consumir APIs, fazer integrações e construir aplicações robustas.
Vamos começar com uma visão geral do ciclo de uma requisição HTTP em Go, passando pela criação da requisição, envio, tratamento da resposta e, finalmente, o fechamento adequado dos recursos. Ao final, você terá uma base sólida para criar clientes HTTP eficientes e seguros.
http.Get e http.Client
A maneira mais rápida de fazer uma requisição HTTP em Go é usar a função http.Get. Ela é um atalho para o método Get do cliente padrão, que é um http.Client global com configurações padrão. Essa função é ideal para testes rápidos ou quando você não precisa de controle fino sobre a requisição.
Veja um exemplo simples que busca uma página e imprime o status e o corpo:
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://api.github.com")
if err != nil {
fmt.Println("Erro:", err)
return
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Erro ao ler corpo:", err)
return
}
fmt.Println("Corpo:", string(body))
}No entanto, http.Get usa o cliente padrão, que não possui timeouts configurados. Em ambientes de produção, é recomendável criar um http.Client personalizado para controlar timeouts, conexões e outros parâmetros. O http.Client é a estrutura central para fazer requisições, pois ele gerencia o transporte, cookies, redirecionamentos e timeouts.
Um http.Client pode ser configurado com um http.Transport personalizado, que controla o pool de conexões, e com timeouts globais. Veja um exemplo simples:
client := &http.Client{
Timeout: 10 * time.Second,
}
resp, err := client.Get("https://api.github.com")Aqui, definimos um timeout de 10 segundos para toda a requisição (incluindo conexão, redirecionamentos e leitura do corpo). Isso evita que o programa fique travado indefinidamente se o servidor não responder.
O http.Client também permite configurar o transporte, por exemplo, para definir limites de conexões ou proxies. Um transporte comum é http.DefaultTransport clonado e modificado:
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.MaxIdleConns = 100
transport.MaxIdleConnsPerHost = 10
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}Isso é útil para otimizar o desempenho em aplicações que fazem muitas requisições.
Requisições customizadas
Para requisições mais complexas, como métodos diferentes de GET (POST, PUT, DELETE), cabeçalhos personalizados, ou corpo, precisamos criar um http.Request manualmente e enviá-lo com o método Do do cliente. A função http.NewRequest cria uma nova requisição com o método, URL e corpo opcional.
Vamos ver um exemplo de uma requisição POST com JSON:
package main
import (
"bytes"
"fmt"
"net/http"
)
func main() {
// Dados JSON a serem enviados
jsonData := []byte(`{"nome":"João", "idade":30}`)
// Cria a requisição
req, err := http.NewRequest("POST", "https://httpbin.org/post", bytes.NewBuffer(jsonData))
if err != nil {
fmt.Println("Erro ao criar requisição:", err)
return
}
// Define cabeçalhos
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token123")
// Envia a requisição
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Erro ao enviar requisição:", err)
return
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
// Processa a resposta...
}Repare que usamos bytes.NewBuffer para criar um leitor a partir dos bytes do JSON. O corpo da requisição pode ser qualquer io.Reader, como um arquivo ou um buffer.
Também podemos personalizar a URL com parâmetros de consulta usando url.Values:
import "net/url"
func main() {
baseURL := "https://api.exemplo.com/search"
params := url.Values{}
params.Add("q", "golang")
params.Add("page", "1")
fullURL := baseURL + "?" + params.Encode()
req, _ := http.NewRequest("GET", fullURL, nil)
// ...
}Isso facilita a construção de URLs com parâmetros de forma segura, evitando erros de escaping.
Outro recurso importante é a capacidade de adicionar cookies à requisição. Podemos usar um http.Cookie e o método AddCookie:
req.AddCookie(&http.Cookie{Name: "session", Value: "abc123"})Ou gerenciar cookies automaticamente com o Jar do cliente, que é um http.CookieJar.
Timeouts
Timeouts são cruciais para que aplicações não fiquem travadas esperando respostas indefinidamente. O http.Client tem um campo Timeout que limita a duração total da requisição (incluindo tempo de conexão, redirecionamentos e leitura do corpo). No entanto, para um controle mais granular, podemos configurar o http.Transport com vários timeouts específicos:
DialContext: timeout para estabelecer a conexão TCP (padrão: 30s).TLSHandshakeTimeout: timeout para o handshake TLS.ResponseHeaderTimeout: tempo para receber os cabeçalhos da resposta.IdleConnTimeout: tempo máximo que uma conexão ociosa permanece no pool.
Exemplo de configuração granular:
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 5 * time.Second,
IdleConnTimeout: 60 * time.Second,
}
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}Esses timeouts ajudam a evitar que uma requisição fique pendente por muito tempo em qualquer estágio. É importante equilibrá-los: se forem muito curtos, podem causar falhas em conexões lentas; se muito longos, podem acumular goroutines presas.
Além disso, podemos usar contextos para cancelamento manual. O método NewRequestWithContext permite associar um context.Context à requisição, que pode ser cancelado ou ter um deadline próprio. Isso é útil para propagar cancelamentos entre chamadas.
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "https://exemplo.com", nil)
resp, err := client.Do(req)
if err != nil {
// Verifica se foi timeout ou cancelamento
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
fmt.Println("Tempo esgotado")
} else {
fmt.Println("Erro:", err)
}
return
}
O contexto é a forma recomendada de controlar cancelamentos e deadlines em Go, e o pacote net/http o integra perfeitamente.
Tratamento de respostas
Depois de enviar uma requisição, a resposta é um *http.Response. É fundamental fechar o corpo (resp.Body) para liberar recursos, geralmente com defer resp.Body.Close(). O corpo é um io.ReadCloser, e podemos ler seu conteúdo de várias maneiras.
Para ler todo o corpo de uma vez, usamos io.ReadAll (ou ioutil.ReadAll em versões anteriores). Mas para respostas grandes, é melhor fazer streaming com um bufio.Scanner ou json.Decoder.
Veja um exemplo de leitura de JSON diretamente do corpo:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func fetchUser(client *http.Client, url string) (*User, error) {
resp, err := client.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status inesperado: %s", resp.Status)
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, err
}
return &user, nil
}Além de ler o corpo, é importante verificar o status code e os cabeçalhos. O campo resp.StatusCode contém o código numérico (200, 404, etc.), e resp.Status a string descritiva. Podemos usar resp.Header.Get("Content-Type") para verificar o tipo de conteúdo.
Para respostas com redirecionamentos, o cliente segue automaticamente até 10 redirecionamentos por padrão. Se quisermos controlar isso, podemos definir um CheckRedirect no cliente:
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 3 {
return errors.New("muitos redirecionamentos")
}
return nil
},
}Isso permite limitar ou modificar o comportamento de redirecionamento.
Outra prática comum é tratar erros de forma robusta. O método client.Do retorna um erro se houver falha de rede, timeout, etc. Mas também podemos obter respostas com status de erro (4xx, 5xx) sem que Do retorne erro. Portanto, sempre verifique o StatusCode e tome ações apropriadas.
Por fim, lembre-se de que o corpo da resposta deve ser fechado mesmo em caso de erro. Use defer logo após a verificação de erro, ou feche explicitamente em todos os caminhos.
Boas práticas
- Sempre feche o corpo da resposta com
defer resp.Body.Close(). - Configure timeouts no cliente para evitar travamentos.
- Use contextos para cancelamento e deadlines quando necessário.
- Reutilize o mesmo
http.Clientpara múltiplas requisições, pois ele mantém o pool de conexões. - Verifique o status code e trate erros de forma explícita.
- Prefira
http.NewRequestWithContextem vez dehttp.NewRequestpara suportar cancelamento.
Referências
- Documentação oficial do pacote net/http
- Documentação do http.Client
- Documentação de http.NewRequest
- Documentação do pacote context
- Artigo oficial sobre context no blog da Go
- Guia de módulos Go (para gerenciar dependências)
Exercícios
Crie um programa que faça uma requisição GET para
https://httpbin.org/status/404e imprima o status code. Depois, verifique se o status é 404 e imprima uma mensagem apropriada.✓ Resposta:package main import ( "fmt" "net/http" ) func main() { resp, err := http.Get("https://httpbin.org/status/404") if err != nil { fmt.Println("Erro:", err) return } defer resp.Body.Close() fmt.Println("Status code:", resp.StatusCode) if resp.StatusCode == http.StatusNotFound { fmt.Println("Recurso não encontrado (404)") } else { fmt.Println("Status inesperado") } }Escreva um código que envie uma requisição POST para
https://httpbin.org/postcom um corpo JSON contendo{"nome":"Maria"}e defina o cabeçalhoContent-Typecomoapplication/json. Imprima o status e a resposta.✓ Resposta:package main import ( "bytes" "fmt" "net/http" ) func main() { jsonData := []byte(`{"nome":"Maria"}`) req, err := http.NewRequest("POST", "https://httpbin.org/post", bytes.NewBuffer(jsonData)) if err != nil { fmt.Println("Erro:", err) return } req.Header.Set("Content-Type", "application/json") client := &http.Client{} resp, err := client.Do(req) if err != nil { fmt.Println("Erro:", err) return } defer resp.Body.Close() fmt.Println("Status:", resp.Status) // Lê o corpo body, _ := io.ReadAll(resp.Body) fmt.Println("Corpo:", string(body)) }Configure um
http.Clientcom timeout de 2 segundos e faça uma requisição parahttps://httpbin.org/delay/5(que atrasa 5 segundos). Trate o erro de timeout e imprima uma mensagem.✓ Resposta:package main import ( "fmt" "net/http" "time" ) func main() { client := &http.Client{ Timeout: 2 * time.Second, } resp, err := client.Get("https://httpbin.org/delay/5") if err != nil { // Verifica se é timeout if netErr, ok := err.(net.Error); ok && netErr.Timeout() { fmt.Println("Timeout: a requisição excedeu 2 segundos") } else { fmt.Println("Erro:", err) } return } defer resp.Body.Close() fmt.Println("Status:", resp.Status) }Crie uma requisição com contexto que tenha deadline de 3 segundos para
https://httpbin.org/delay/5. Usehttp.NewRequestWithContexte capture o erro de deadline.✓ Resposta:package main import ( "context" "errors" "fmt" "net/http" "time" ) func main() { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/delay/5", nil) if err != nil { fmt.Println("Erro ao criar requisição:", err) return } client := &http.Client{} resp, err := client.Do(req) if err != nil { if errors.Is(ctx.Err(), context.DeadlineExceeded) { fmt.Println("Deadline excedida (timeout)") } else { fmt.Println("Erro:", err) } return } defer resp.Body.Close() fmt.Println("Status:", resp.Status) }Faça uma requisição GET para
https://api.github.come decodifique a resposta JSON em um map ou struct. Imprima o valor do campocurrent_user_url(ou outro campo).✓ Resposta:package main import ( "encoding/json" "fmt" "net/http" ) func main() { resp, err := http.Get("https://api.github.com") if err != nil { fmt.Println("Erro:", err) return } defer resp.Body.Close() var data map[string]interface{} if err := json.NewDecoder(resp.Body).Decode(&data); err != nil { fmt.Println("Erro ao decodificar JSON:", err) return } if url, ok := data["current_user_url"].(string); ok { fmt.Println("current_user_url:", url) } else { fmt.Println("Campo não encontrado") } }