Cliente HTTP
Esta aula ensina a criar clientes HTTP em Go, desde requisições simples com http.Get até o uso avançado de http.Client, customização de requisições, configuração de timeouts e tratamento adequado de respostas. Inclui exemplos práticos, boas práticas, referências e exercícios com respostas.
Nesta aula, você aprenderá a consumir APIs e serviços web usando a biblioteca padrão do Go. O pacote net/http fornece ferramentas poderosas para criar clientes HTTP, permitindo desde requisições simples até controle fino de conexões, timeouts e headers. Dominar essas técnicas é essencial para qualquer desenvolvedor Go que precise integrar sistemas ou acessar recursos remotos.
Vamos explorar desde o uso direto de http.Get até a criação de um http.Client personalizado, passando por requisições customizadas, gerenciamento de timeouts e tratamento de respostas. Ao final, você terá uma base sólida para construir clientes HTTP robustos e eficientes em seus projetos.
http.Get e http.Client
O Go oferece duas formas principais de fazer requisições HTTP: a função de conveniência http.Get e o tipo http.Client. A primeira é ideal para requisições simples e rápidas, enquanto a segunda oferece mais controle e reutilização de configurações.
Com http.Get, você faz uma requisição GET e recebe uma resposta em uma única chamada. No entanto, ela usa o client padrão (http.DefaultClient), que não possui timeouts específicos e pode não ser adequado para produção. Já http.Client permite definir timeouts, transportes personalizados, e reutilizar conexões, tornando-o a escolha recomendada para aplicações reais.
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
// Usando http.Get
resp, err := http.Get("https://api.github.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body)[:100])
// Usando http.Client
client := &http.Client{}
resp2, err := client.Get("https://api.github.com")
if err != nil {
panic(err)
}
defer resp2.Body.Close()
body2, _ := io.ReadAll(resp2.Body)
fmt.Println(string(body2)[:100])
}Note que http.Get é um atalho para http.DefaultClient.Get. Em aplicações reais, prefira criar um client com configurações apropriadas.
Requisições customizadas
Para requisições que exigem métodos diferentes, headers personalizados ou corpo, você precisa criar uma http.Request manualmente e usar o método Do do client. Isso permite total controle sobre a requisição.
Você pode usar http.NewRequest para criar a requisição, especificando método, URL e corpo (que deve ser um io.Reader). Depois, defina headers e outros atributos antes de enviar com client.Do.
package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
client := &http.Client{}
jsonBody := []byte(`{"name":"John"}`)
req, err := http.NewRequest("POST", "https://httpbin.org/post", bytes.NewBuffer(jsonBody))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}Além de headers, você pode definir parâmetros de query usando req.URL.Query() e adicionar valores. Também é possível configurar cookies, autenticação básica e até mesmo cancelar a requisição com context.
Timeouts
Timeouts são cruciais para evitar que seu programa fique pendurado indefinidamente esperando uma resposta. O http.Client permite configurar timeouts em dois níveis: o timeout total da requisição (Timeout) e o timeout de conexão (Transport.DialContext).
O campo Timeout do client define o tempo máximo para a requisição inteira, incluindo leitura da resposta. Para um controle mais granular, você pode usar context.Context com cancelamento ou deadlines. Aqui está um exemplo com timeout total:
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
client := &http.Client{
Timeout: 5 * time.Second,
}
resp, err := client.Get("https://httpbin.org/delay/10")
if err != nil {
fmt.Println("Erro:", err)
return
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}Se você precisar de um timeout apenas para a conexão (não para a resposta), pode configurar o Transport. Isso é útil para evitar espera longa quando o servidor não responde ao handshake TCP.
package main
import (
"fmt"
"net"
"net/http"
"time"
)
func main() {
transport := &http.Transport{
DialContext: (&net.Dialer{
Timeout: 2 * time.Second,
}).DialContext,
}
client := &http.Client{
Transport: transport,
Timeout: 10 * time.Second,
}
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
fmt.Println("Erro:", err)
return
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}Além disso, o uso de context permite cancelar requisições manualmente ou definir deadlines mais flexíveis, como veremos a seguir.
Tratamento de respostas
Após receber a resposta, é importante verificar o status code, ler o corpo corretamente e fechar o corpo. O corpo da resposta deve ser fechado para reutilizar conexões.
O campo resp.StatusCode contém o código HTTP, e resp.Status inclui o texto. Você pode usar io.ReadAll para ler todo o corpo, mas para respostas grandes, é melhor usar json.Decoder ou bufio.Scanner para processar incrementalmente.
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type Response struct {
Origin string `json:"origin"`
}
func main() {
client := &http.Client{}
resp, err := client.Get("https://httpbin.org/ip")
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Erro: status %d\n", resp.StatusCode)
return
}
var data Response
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
panic(err)
}
fmt.Printf("IP: %s\n", data.Origin)
}É uma boa prática verificar o status code e tratar erros de leitura. Além disso, lembre-se de fechar o corpo com defer para evitar vazamento de recursos.
Boas práticas e observações finais
Ao trabalhar com clientes HTTP em Go, considere as seguintes práticas:
- Reutilize
http.Clientem vez de criar um a cada requisição, pois ele gerencia conexões e timeouts de forma eficiente. - Defina timeouts sempre, seja via
Client.Timeoutou viacontext, para evitar bloqueios indefinidos. - Use
contextpara cancelar requisições quando necessário, especialmente em servidores ou operações de longa duração. - Verifique o status code da resposta e trate erros adequadamente, não apenas a ausência de erro na chamada.
- Feche o corpo da resposta (
defer resp.Body.Close()) para permitir reutilização da conexão. - Para requisições com corpo, use
bytes.Bufferoustrings.Readercomoio.Reader.
Essas práticas garantem que seu cliente HTTP seja robusto e confiável em ambientes de produção.
Referências
- Documentação oficial do pacote net/http
- Usando context no Go (blog oficial)
- Como definir timeouts em requisições HTTP (Alex Edwards)
- Usando o client HTTP do Go (Medium)
- Documentação do tipo http.Client
- httpbin - serviço de teste de requisições HTTP
Exercícios
- Exercício 1: Crie um programa que faça uma requisição GET para
https://api.github.com/users/octocate imprima o nome de usuário e o id. Usehttp.Geteio.ReadAll. - Exercício 2: Crie um cliente HTTP com timeout de 3 segundos e faça uma requisição para
https://httpbin.org/delay/2. Imprima o status e o corpo. - Exercício 3: Faça uma requisição POST para
https://httpbin.org/postcom um corpo JSON contendo{"message":"hello"}e headerContent-Type: application/json. Imprima o corpo da resposta. - Exercício 4: Crie uma requisição que use
contextcom timeout de 1 segundo parahttps://httpbin.org/delay/5. Trate o erro de timeout. - Exercício 5: Faça uma requisição GET para
https://api.github.com/repos/golang/goe, se o status for 200, decodifique o JSON para extrair o campohtml_urle imprima-o. Usejson.Decoder.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
)
func main() {
resp, err := http.Get("https://api.github.com/users/octocat")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var user struct {
Login string `json:"login"`
ID int `json:"id"`
}
json.Unmarshal(body, &user)
fmt.Printf("Login: %s, ID: %d\n", user.Login, user.ID)
}package main
import (
"fmt"
"io"
"net/http"
"time"
)
func main() {
client := &http.Client{
Timeout: 3 * time.Second,
}
resp, err := client.Get("https://httpbin.org/delay/2")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Status: %s\n", resp.Status)
fmt.Println("Body:", string(body))
}package main
import (
"bytes"
"fmt"
"io"
"net/http"
)
func main() {
client := &http.Client{}
payload := []byte(`{"message":"hello"}`)
req, err := http.NewRequest("POST", "https://httpbin.org/post", bytes.NewBuffer(payload))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://httpbin.org/delay/5", nil)
if err != nil {
panic(err)
}
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)
}package main
import (
"encoding/json"
"fmt"
"net/http"
)
func main() {
client := &http.Client{}
resp, err := client.Get("https://api.github.com/repos/golang/go")
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
fmt.Printf("Erro: status %d\n", resp.StatusCode)
return
}
var repo struct {
HTMLURL string `json:"html_url"`
}
err = json.NewDecoder(resp.Body).Decode(&repo)
if err != nil {
panic(err)
}
fmt.Println("URL:", repo.HTMLURL)
}