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.Client em vez de criar um a cada requisição, pois ele gerencia conexões e timeouts de forma eficiente.
  • Defina timeouts sempre, seja via Client.Timeout ou via context, para evitar bloqueios indefinidos.
  • Use context para 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.Buffer ou strings.Reader como io.Reader.

Essas práticas garantem que seu cliente HTTP seja robusto e confiável em ambientes de produção.

Referências

Exercícios

  1. Exercício 1: Crie um programa que faça uma requisição GET para https://api.github.com/users/octocat e imprima o nome de usuário e o id. Use http.Get e io.ReadAll.
  2. ✓ Resposta:
    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)
    }
  3. 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.
  4. ✓ Resposta:
    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))
    }
  5. Exercício 3: Faça uma requisição POST para https://httpbin.org/post com um corpo JSON contendo {"message":"hello"} e header Content-Type: application/json. Imprima o corpo da resposta.
  6. ✓ Resposta:
    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))
    }
  7. Exercício 4: Crie uma requisição que use context com timeout de 1 segundo para https://httpbin.org/delay/5. Trate o erro de timeout.
  8. ✓ Resposta:
    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)
    }
  9. Exercício 5: Faça uma requisição GET para https://api.github.com/repos/golang/go e, se o status for 200, decodifique o JSON para extrair o campo html_url e imprima-o. Use json.Decoder.
  10. ✓ Resposta:
    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)
    }