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.Client para 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.NewRequestWithContext em vez de http.NewRequest para suportar cancelamento.

Referências

Exercícios

  1. Crie um programa que faça uma requisição GET para https://httpbin.org/status/404 e 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")
        }
    }
  2. Escreva um código que envie uma requisição POST para https://httpbin.org/post com um corpo JSON contendo {"nome":"Maria"} e defina o cabeçalho Content-Type como application/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))
    }
  3. Configure um http.Client com timeout de 2 segundos e faça uma requisição para https://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)
    }
  4. Crie uma requisição com contexto que tenha deadline de 3 segundos para https://httpbin.org/delay/5. Use http.NewRequestWithContext e 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)
    }
  5. Faça uma requisição GET para https://api.github.com e decodifique a resposta JSON em um map ou struct. Imprima o valor do campo current_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")
        }
    }