Skip to main content


HTTP em Go​


HTTP, que significa Hypertext Transfer Protocol, é um protocolo de aplicação usado para a comunicação entre clientes e servidores. Ele define como uma aplicação pode enviar uma requisição para outro sistema e receber uma resposta, permitindo a troca de informações pela rede. Esse protocolo é usado em sites, APIs, serviços web e ferramentas que precisam consultar ou enviar dados para servidores.


Em Go, podemos trabalhar com HTTP usando o pacote net/http, que faz parte da biblioteca padrão. Esse pacote fornece recursos para criar clientes HTTP, enviar requisições, interpretar respostas e desenvolver servidores capazes de receber e processar diferentes tipos de solicitações. Isso permite implementar boa parte das operações HTTP sem depender de bibliotecas externas.


Uma comunicação HTTP normalmente começa quando o cliente envia uma requisição contendo um método, um endereço e informações adicionais. O servidor recebe essa requisição, executa a operação correspondente e devolve uma resposta com um código de status, headers e, quando necessário, um corpo com dados. Esses elementos permitem que os dois lados entendam qual operação foi solicitada e qual foi seu resultado.


O protocolo HTTP não define sozinho a proteção criptográfica da comunicação. Quando usamos HTTPS, os dados HTTP são transmitidos por uma conexão protegida com TLS, que fornece mecanismos de criptografia e autenticação. Em Go, o pacote net/http permite trabalhar com HTTP e HTTPS, enquanto a biblioteca padrão cuida de boa parte dos detalhes necessários para estabelecer essas comunicações.



Fazendo requisições HTTP​


Uma requisição HTTP é uma mensagem enviada por um cliente para solicitar alguma operação a um servidor. Essa operação pode representar a consulta de uma página, a leitura de informações de uma API ou o envio de dados que serão processados. O servidor recebe a requisição e devolve uma resposta seguindo as regras do protocolo HTTP.


Cada requisição possui um método, que representa a operação solicitada, e um endereço que identifica o recurso acessado. Também pode conter headers, responsáveis por transportar informações adicionais, e um corpo, usado quando precisamos enviar dados. A resposta normalmente contém um código de status, headers e um corpo que pode representar texto, HTML, JSON ou outros tipos de conteúdo.


Em Go, podemos enviar requisições usando o pacote net/http. Ele possui funções simples, como http.Get, e estruturas que permitem controlar diferentes aspectos da comunicação, como http.Client e http.Request. Para começar, vamos enviar uma requisição GET e verificar o resultado retornado pelo servidor.



Fazendo uma requisição com http.Get​


A função http.Get permite enviar uma requisição HTTP usando o método GET, que normalmente é empregado para consultar ou recuperar informações de um recurso. Ela recebe uma URL e retorna um ponteiro para http.Response, que representa a resposta do servidor, e um possível erro. A função usa o cliente HTTP padrão da biblioteca, permitindo executar requisições simples com pouco código.


Uma URL, ou Uniform Resource Locator, identifica a localização de um recurso e pode conter informações como protocolo, domínio, porta, caminho e parâmetros de consulta. Por exemplo, em https://example.com, o prefixo https identifica o esquema da comunicação, enquanto example.com representa o domínio usado para localizar o servidor.

package main

import (
"fmt"
"net/http"
"os"
)

func main() {
// Envia uma requisição GET para o endereço informado.
resp, err := http.Get("https://example.com")

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na requisição:",
err,
)

return
}

// Fecha o corpo da resposta quando a função terminar.
defer resp.Body.Close()

// Exibe o status retornado pelo servidor.
fmt.Println("Status:", resp.Status)
fmt.Println("Código:", resp.StatusCode)
}

A função http.Get recebe o endereço que será consultado e realiza as operações necessárias para enviar a requisição. Isso pode incluir a resolução do domínio para um endereço IP, o estabelecimento da conexão e a negociação TLS quando usamos HTTPS. Depois que o servidor responde, a função retorna um objeto http.Response, permitindo acessar as informações recebidas.


O campo resp.Status contém o código de status acompanhado de sua descrição textual, enquanto resp.StatusCode representa apenas o valor numérico. Em uma requisição bem-sucedida para uma página disponível, podemos receber o código 200, que representa OK. Entretanto, o resultado depende do servidor e pode mudar conforme o recurso consultado e as condições da comunicação.


A instrução defer resp.Body.Close() registra o fechamento do corpo da resposta para quando a função terminar. Esse fechamento é necessário porque a resposta pode manter recursos associados à comunicação, incluindo conexões que poderão ser reaproveitadas pelo cliente HTTP. Para favorecer esse reaproveitamento, normalmente também precisamos consumir o corpo até o final antes de fechá-lo.


A função http.Get é conveniente para exemplos pequenos, mas usa o cliente HTTP padrão, que não possui um timeout global configurado. Isso significa que uma operação pode permanecer aguardando por um período maior do que o desejado. Para ferramentas de linha de comando e aplicações que dependem de serviços externos, normalmente é melhor criar um cliente com configurações explícitas.



Criando um cliente HTTP​


O tipo http.Client representa um cliente responsável por enviar requisições HTTP e administrar comportamentos relacionados à comunicação. Por meio dele, podemos configurar limites de tempo, redirecionamentos e outros parâmetros. O cliente também trabalha com um mecanismo chamado Transport, que é responsável pelas operações de transporte das requisições, incluindo o gerenciamento e o reaproveitamento de conexões.


Podemos criar um cliente informando um timeout, que representa o tempo máximo permitido para a execução de uma requisição. Depois disso, usamos métodos como Get, Post ou Do para enviar as solicitações. O método Do recebe um objeto http.Request, permitindo maior controle sobre o método, os headers e o corpo enviados.

package main

import (
"fmt"
"io"
"net/http"
"os"
"time"
)

func main() {
// Cria um cliente com limite de cinco segundos.
client := &http.Client{
Timeout: 5 * time.Second,
}

// Envia uma requisição usando o cliente configurado.
resp, err := client.Get("https://example.com")

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na requisição:",
err,
)

return
}

defer resp.Body.Close()

// Consome o corpo para permitir seu reaproveitamento normal.
_, err = io.Copy(io.Discard, resp.Body)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao ler resposta:",
err,
)

return
}

fmt.Println("Status:", resp.Status)
}

Nesse exemplo, criamos um cliente usando &http.Client e definimos um timeout de cinco segundos. O método client.Get executa a requisição usando essa configuração, permitindo que a operação seja interrompida caso o tempo máximo seja ultrapassado. Esse limite inclui diferentes etapas da comunicação, como o estabelecimento da conexão, possíveis redirecionamentos e a leitura do corpo da resposta.


A função io.Copy copia os dados recebidos de resp.Body para io.Discard, que representa um destino que descarta as informações. Usamos essa operação porque o exemplo precisa apenas consultar o status, sem apresentar o conteúdo retornado. Quando o corpo é lido completamente e fechado, o transporte pode reaproveitar a conexão para outras requisições, dependendo das condições da comunicação.


O mesmo http.Client pode ser compartilhado por diferentes goroutines, porque seus métodos foram desenvolvidos para permitir acesso concorrente. Também é recomendado reaproveitar clientes e transportes quando possível, em vez de criar uma nova instância para cada requisição. Isso facilita o gerenciamento das conexões e evita trabalho desnecessário durante a comunicação com servidores consultados repetidamente.


O cliente HTTP não representa uma conexão TCP exclusiva. Ele pode abrir, manter, reaproveitar ou encerrar conexões conforme as necessidades das requisições e as regras do transporte. Essa diferença permite realizar várias operações HTTP usando o mesmo cliente, sem precisar estabelecer manualmente uma conexão de rede para cada solicitação.



Criando uma requisição com http.NewRequest​


Quando precisamos controlar o método, os headers ou os dados enviados, podemos criar uma requisição explicitamente usando http.NewRequest. Essa função recebe o método HTTP, a URL e um valor que representa o corpo da requisição. Ela retorna um objeto http.Request, que pode ser configurado antes de ser enviado por http.Client.Do.


O corpo pode ser informado por um valor que implementa io.Reader, uma interface usada para representar fontes de dados que podem ser lidas. Quando a requisição não precisa enviar um corpo, podemos informar nil. Depois de criar o objeto, usamos o método Do do cliente para executar a comunicação.

package main

import (
"fmt"
"net/http"
"os"
"time"
)

func main() {
client := &http.Client{
Timeout: 5 * time.Second,
}

// Cria uma requisição GET sem corpo.
req, err := http.NewRequest(
http.MethodGet,
"https://example.com",
nil,
)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao criar requisição:",
err,
)

return
}

// Executa a requisição criada anteriormente.
resp, err := client.Do(req)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao executar requisição:",
err,
)

return
}

defer resp.Body.Close()

fmt.Println("Status:", resp.Status)
}

A constante http.MethodGet representa o método HTTP GET, evitando escrever seu nome diretamente como uma string. O terceiro argumento de http.NewRequest recebe nil porque não precisamos enviar dados no corpo dessa requisição. A função pode retornar um erro quando a URL ou outros parâmetros não permitem criar uma requisição válida, por isso verificamos seu resultado antes de chamar client.Do.


O método client.Do recebe o objeto criado e executa a requisição. Essa separação entre criação e execução permite modificar headers, configurar autenticação e associar um contexto antes de iniciar a comunicação. Também permite criar funções responsáveis por montar requisições de maneira padronizada, deixando o envio sob responsabilidade de outra parte do programa.


Assim como acontece com http.Get, o retorno de client.Do precisa ser tratado corretamente. Um erro pode representar uma falha de conexão, um problema no protocolo ou alguma condição associada à execução da requisição. Entretanto, códigos HTTP como 404 e 500 normalmente não fazem com que client.Do retorne um erro, porque representam respostas HTTP válidas que precisam ser analisadas separadamente.



Enviando headers​


Headers HTTP são campos que transportam informações adicionais sobre uma requisição ou resposta. Eles permitem indicar características do conteúdo, preferências do cliente, credenciais de autenticação e outras informações necessárias para a comunicação. Cada header possui um nome e um ou mais valores, seguindo as regras definidas pelo protocolo HTTP.


Em uma requisição, por exemplo, o header Accept permite informar quais tipos de conteúdo o cliente prefere receber, enquanto Content-Type descreve o formato do corpo enviado. O header Authorization pode transportar informações de autenticação e User-Agent identifica o software responsável pela requisição. Esses campos possuem responsabilidades diferentes e não devem ser tratados como configurações equivalentes.


Em Go, os headers são representados pelo tipo http.Header, que funciona como um mapa de nomes associados a listas de strings. O objeto http.Request possui o campo Header, permitindo adicionar, modificar e consultar essas informações antes do envio. Para isso, podemos usar métodos como Set, Add, Get e Del.



Adicionando headers em uma requisição​


Para adicionar um header, primeiro precisamos criar uma requisição usando http.NewRequest. Depois, podemos chamar req.Header.Set, informando o nome do campo e o valor desejado. Quando o header já possui valores definidos, Set substitui esses valores por um único valor novo, evitando acumular informações anteriores sem necessidade.


Vamos criar uma requisição GET que informa ao servidor a preferência por respostas JSON e identifica o programa responsável pela comunicação. Essas informações serão adicionadas antes de executar a requisição com client.Do.

package main

import (
"fmt"
"net/http"
"os"
"time"
)

func main() {
client := &http.Client{
Timeout: 5 * time.Second,
}

// Cria a requisição HTTP.
req, err := http.NewRequest(
http.MethodGet,
"https://example.com",
nil,
)

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

// Informa os tipos de conteúdo aceitos pelo cliente.
req.Header.Set(
"Accept",
"application/json",
)

// Identifica a ferramenta responsável pela requisição.
req.Header.Set(
"User-Agent",
"redecli/1.0",
)

// Envia a requisição com os headers configurados.
resp, err := client.Do(req)

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

defer resp.Body.Close()

fmt.Println("Status:", resp.Status)
}

O header Accept informa que o cliente prefere receber dados no formato JSON, representado pelo media type application/json. Um media type identifica o tipo de conteúdo transportado em uma mensagem, permitindo que o destinatário saiba como interpretá-lo. Entretanto, informar Accept não garante que o servidor responderá nesse formato, pois ele pode escolher outro conteúdo ou retornar um erro indicando que não consegue atender à preferência.


O header User-Agent identifica a aplicação que está enviando a requisição. Nesse exemplo, usamos redecli/1.0, mas o valor pode seguir a identificação escolhida para a ferramenta. O servidor pode usar essa informação em registros ou diagnósticos, embora não deva considerá-la uma prova confiável da identidade do cliente, porque o campo pode ser modificado livremente.


Também precisamos diferenciar a criação da requisição de seu envio. Os métodos usados em req.Header apenas modificam o objeto mantido em memória. Os headers serão enviados quando chamarmos client.Do(req), permitindo configurar todas as informações necessárias antes de iniciar a comunicação.



Diferença entre Set e Add​


O tipo http.Header permite associar mais de um valor ao mesmo nome de header. O método Set substitui os valores anteriores por um único valor, enquanto Add acrescenta outro valor à lista existente. Essa diferença pode ser necessária para headers que permitem múltiplos valores, mas depende das regras específicas de cada campo HTTP.


Podemos demonstrar esse comportamento usando um objeto http.Header independente, sem precisar executar uma requisição de rede. Nesse exemplo, adicionamos dois valores ao mesmo header e consultamos a lista armazenada.

package main

import (
"fmt"
"net/http"
)

func main() {
// Cria um conjunto de headers.
headers := make(http.Header)

// Adiciona dois valores ao mesmo campo.
headers.Add("X-Teste", "valor-1")
headers.Add("X-Teste", "valor-2")

// Retorna todos os valores associados ao campo.
fmt.Println(headers.Values("X-Teste"))

// Substitui os valores anteriores.
headers.Set("X-Teste", "novo-valor")

fmt.Println(headers.Values("X-Teste"))
}

A saída será:

[valor-1 valor-2]
[novo-valor]

O método Values retorna os valores associados ao nome informado, permitindo consultar todos os elementos armazenados. Já Get retorna apenas o primeiro valor associado ao header ou uma string vazia quando o campo não existe. Os nomes são tratados sem diferenciação entre letras maiúsculas e minúsculas pelos métodos normais de http.Header, seguindo o comportamento esperado para nomes de campos HTTP.


Também podemos remover um header usando Del, que exclui os valores associados ao nome informado. Entretanto, nem todos os headers permitem múltiplos valores com o mesmo significado, por isso devemos seguir as regras de cada campo. O uso de Add não significa automaticamente que o servidor interpretará todos os valores como uma lista ou que eles poderão ser combinados sem alterar seu significado.



Enviando informações de autenticação​


Algumas APIs exigem que o cliente envie informações de autenticação antes de permitir o acesso a determinados recursos. Um mecanismo comum é o uso do header Authorization, que pode transportar um token associado à identidade ou às permissões do cliente. O formato depende do esquema de autenticação exigido pelo servidor, como Bearer ou Basic.


Quando uma API trabalha com tokens Bearer, podemos adicionar o header usando req.Header.Set. O token normalmente deve ser recebido de uma configuração segura, variável de ambiente ou mecanismo próprio de gerenciamento de credenciais, evitando armazenar informações sensíveis diretamente no código.

// Obtém o token definido no ambiente.
token := os.Getenv("API_TOKEN")

if token == "" {
return errors.New("API_TOKEN não foi definido")
}

// Adiciona o token à requisição já criada.
req.Header.Set(
"Authorization",
"Bearer "+token,
)

Esse trecho pressupõe que req contém uma requisição HTTP válida, que os pacotes os e errors foram importados e que o código está dentro de uma função que retorna error. O header será enviado quando a requisição for executada, permitindo que o servidor avalie o token recebido conforme suas regras de autenticação.


Informações de autenticação não devem ser transmitidas por HTTP sem proteção quando a comunicação passa por redes não confiáveis. Para APIs que trabalham com tokens, senhas ou outros dados sensíveis, normalmente devemos usar HTTPS e manter a validação dos certificados habilitada. Também precisamos evitar imprimir tokens em mensagens de diagnóstico ou registros, porque esses valores podem permitir acesso aos recursos protegidos.


Alguns headers possuem tratamento especial no pacote net/http. O campo Host, por exemplo, não deve ser configurado apenas por req.Header.Set("Host", ...) quando precisamos alterar o host enviado pela requisição. Nesse caso, usamos o campo req.Host, que possui uma responsabilidade própria na representação das requisições HTTP.



Trabalhando com GET e POST​


GET e POST são métodos HTTP usados para representar operações diferentes. O método GET normalmente solicita a representação de um recurso, sem pedir que o servidor modifique seu estado. Já POST é usado para enviar informações que serão processadas pelo recurso de destino, podendo provocar a criação de dados ou outras alterações definidas pela aplicação.


Essa diferença influencia a maneira como os parâmetros são enviados. Em requisições GET, é comum trabalhar com informações de consulta presentes na URL, enquanto requisições POST frequentemente transportam dados no corpo da mensagem. Entretanto, o protocolo não exige que todo GET tenha parâmetros na URL nem que todo POST use JSON, porque o formato do conteúdo depende das regras do serviço acessado.


O método GET é considerado seguro pela semântica HTTP, pois representa uma operação que não solicita alteração de estado. POST não possui essa garantia e também não é considerado idempotente por padrão. Uma operação idempotente é aquela cujo efeito pretendido permanece o mesmo quando a mesma solicitação é executada repetidamente, característica relevante quando precisamos decidir se uma requisição pode ser repetida após uma falha.



Enviando parâmetros em uma requisição GET​


Uma URL pode possuir parâmetros de consulta, também conhecidos como query parameters, que permitem enviar informações adicionais junto ao endereço do recurso. Esses parâmetros aparecem depois do caractere ? e normalmente são representados por pares de nomes e valores. Quando existem vários parâmetros, eles são separados pelo caractere &.


Podemos consultar um endpoint que recebe um nome e uma quantidade usando uma URL semelhante a esta:

http://127.0.0.1:8080/consulta?nome=go&limite=10

Nesse exemplo, o caminho /consulta identifica o recurso solicitado, enquanto nome=go e limite=10 representam os parâmetros enviados. Embora possamos montar essa string manualmente, o procedimento pode provocar erros quando os valores contêm espaços, caracteres especiais ou símbolos que possuem significado na estrutura da URL.


O pacote net/url fornece recursos para trabalhar com esses parâmetros de maneira adequada. Podemos usar url.Values e o método Encode, responsável por converter os valores para uma representação compatível com a sintaxe de consultas em URLs.

package main

import (
"fmt"
"net/http"
"net/url"
"os"
"time"
)

func main() {
// Define os parâmetros que serão enviados.
parametros := url.Values{}

parametros.Set("nome", "Go linguagem")
parametros.Set("limite", "10")

// Monta a URL com os parâmetros codificados.
endereco := "http://127.0.0.1:8080/consulta?"
endereco += parametros.Encode()

client := &http.Client{
Timeout: 5 * time.Second,
}

// Envia a requisição GET.
resp, err := client.Get(endereco)

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

defer resp.Body.Close()

fmt.Println("URL:", endereco)
fmt.Println("Status:", resp.Status)
}

O tipo url.Values representa um conjunto de parâmetros associados a nomes, permitindo armazenar um ou mais valores para cada campo. O método Set define o valor de determinado parâmetro, enquanto Encode gera sua representação codificada. No exemplo, o espaço presente em Go linguagem será representado por +, seguindo as regras de codificação usadas em parâmetros de formulário de URLs.


A URL gerada será equivalente a esta:

http://127.0.0.1:8080/consulta?limite=10&nome=Go+linguagem

O método Encode organiza os parâmetros por nome, por isso a ordem dos campos pode ser diferente daquela em que foram adicionados. Essa diferença normalmente não modifica o significado da consulta, porque os valores são identificados pelos seus nomes. O servidor precisa possuir um endpoint /consulta para responder à requisição, que será criado posteriormente neste capítulo.


Também precisamos considerar que parâmetros presentes na URL podem aparecer em históricos, registros de servidores e ferramentas de diagnóstico. Por isso, não devemos enviar senhas ou tokens sensíveis em query parameters sem uma razão específica e uma avaliação adequada dos riscos. Para autenticação, normalmente usamos mecanismos próprios, como headers enviados por conexões HTTPS.



Enviando dados com POST​


O método POST permite enviar dados para um recurso que será responsável por interpretá-los e executar determinada operação. Uma API pode usar POST para receber informações de cadastro, processar documentos ou executar outras ações definidas pelo serviço. O formato do corpo precisa ser conhecido pelos dois lados da comunicação para que os dados sejam interpretados corretamente.


Um formato bastante comum em APIs HTTP é JSON, que permite representar objetos, listas, strings, números e valores booleanos. Em Go, podemos converter uma estrutura para JSON usando o pacote encoding/json, que fornece a função json.Marshal. O resultado dessa conversão pode ser enviado no corpo da requisição usando um valor que implementa io.Reader.


Vamos criar um programa que envia um objeto JSON para um endpoint local chamado /mensagens. O servidor que receberá esse conteúdo será implementado posteriormente, permitindo testar a comunicação entre o cliente e a aplicação HTTP criada em Go.

package main

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)

type Mensagem struct {
Texto string `json:"texto"`
}

func main() {
// Define os dados que serão enviados.
mensagem := Mensagem{
Texto: "Olá servidor",
}

// Converte a estrutura para JSON.
dados, err := json.Marshal(mensagem)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao gerar JSON:",
err,
)

return
}

// Cria uma requisição POST contendo os dados.
req, err := http.NewRequest(
http.MethodPost,
"http://127.0.0.1:8080/mensagens",
bytes.NewReader(dados),
)

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

// Informa o formato do corpo enviado.
req.Header.Set(
"Content-Type",
"application/json",
)

client := &http.Client{
Timeout: 5 * time.Second,
}

// Executa a requisição.
resp, err := client.Do(req)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao enviar dados:",
err,
)

return
}

defer resp.Body.Close()

fmt.Println("Status:", resp.Status)
}

A estrutura Mensagem representa o conteúdo que será enviado, enquanto a tag json:"texto" define o nome do campo na representação JSON. A função json.Marshal recebe a estrutura e retorna um slice de bytes contendo os dados convertidos. Nesse caso, o corpo enviado terá um conteúdo equivalente a {"texto":"Olá servidor"}, seguindo a representação definida pelo pacote.


A função bytes.NewReader cria um leitor a partir do slice de bytes, permitindo que http.NewRequest receba os dados como corpo da requisição. O método http.MethodPost indica que a operação solicitada usa POST, enquanto o header Content-Type informa que o conteúdo está representado em JSON. Esse header é necessário para que o servidor possa identificar o formato esperado antes de interpretar o corpo.


Depois de criar a requisição, usamos client.Do para enviá-la ao servidor. A execução poderá retornar um erro caso a comunicação falhe, mas um código HTTP de erro, como 400 ou 500, normalmente será recebido como uma resposta válida. Por isso, precisamos consultar o status retornado para identificar se o servidor aceitou ou rejeitou os dados enviados.


Também é possível enviar JSON usando diretamente http.Client.Post, que recebe a URL, o tipo de conteúdo e o leitor responsável pelo corpo. Entretanto, criar a requisição com http.NewRequest permite configurar outros headers e associar um contexto de cancelamento antes do envio. Essa abordagem costuma ser mais conveniente quando estamos desenvolvendo clientes para APIs que exigem autenticação ou outras configurações.



Enviando formulários com POST​


Nem todas as requisições POST transportam JSON. Algumas aplicações recebem dados no formato application/x-www-form-urlencoded, em que os campos são representados por pares de nomes e valores codificados. Esse formato é comum em formulários HTML tradicionais e também pode ser aceito por determinados endpoints de APIs.


O pacote net/http possui o método PostForm, que permite enviar esses valores sem precisar montar manualmente o corpo da requisição. Ele recebe a URL e um valor do tipo url.Values, convertendo os dados para a representação esperada e definindo automaticamente o header Content-Type correspondente.

package main

import (
"fmt"
"net/http"
"net/url"
"os"
"time"
)

func main() {
// Define os campos do formulário.
formulario := url.Values{
"nome": {"Go"},
"email": {"teste@example.com"},
}

client := &http.Client{
Timeout: 5 * time.Second,
}

// Envia os campos usando POST.
resp, err := client.PostForm(
"http://127.0.0.1:8080/formulario",
formulario,
)

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

defer resp.Body.Close()

fmt.Println("Status:", resp.Status)
}

O método PostForm converte os campos para o formato esperado pelo servidor e envia a requisição usando POST. Os valores são codificados conforme as regras de formulários em URLs, permitindo representar espaços e outros caracteres especiais. O servidor precisa conhecer os nomes dos campos e o formato enviado para interpretar as informações corretamente.


Essa função é conveniente para requisições simples, mas não deve ser confundida com o envio de formulários multipart/form-data, frequentemente usados para transportar arquivos. O formato multipart possui uma estrutura diferente e exige mecanismos próprios para construir e interpretar suas partes. Quando precisamos adicionar headers personalizados ou controlar o cancelamento, também podemos criar uma requisição manualmente em vez de usar PostForm.



Lendo respostas HTTP​


Depois que uma requisição é enviada, o servidor devolve uma resposta HTTP contendo informações sobre o resultado da operação. Essa resposta normalmente possui um código de status, headers e um corpo com os dados retornados. Em Go, essas informações ficam disponíveis no objeto http.Response, recebido por funções como http.Get e pelo método http.Client.Do.


A leitura da resposta é uma etapa diferente do envio da requisição. Quando client.Do retorna com sucesso, normalmente os headers já foram recebidos, mas o corpo pode ainda não ter sido lido completamente. Isso permite processar respostas grandes de maneira gradual, sem precisar armazenar todo o conteúdo na memória antes de começar a trabalhar com ele.


O corpo da resposta é representado pelo campo Body, que implementa io.ReadCloser. Essa interface combina operações de leitura e fechamento, permitindo receber os dados e liberar os recursos associados quando não forem mais necessários. Precisamos fechar esse corpo mesmo quando não pretendemos processar seu conteúdo, evitando manter recursos ocupados desnecessariamente.



Interpretando códigos de status HTTP​


O código de status HTTP representa o resultado da operação realizada pelo servidor. Ele é composto por três dígitos, organizados em categorias que indicam respostas informativas, sucesso, redirecionamento, erros do cliente e erros do servidor. Esses códigos fazem parte do protocolo e permitem que o cliente identifique o resultado sem depender exclusivamente do conteúdo retornado.


Alguns códigos aparecem com frequência durante a comunicação com APIs e serviços HTTP. Podemos organizar os principais exemplos da seguinte maneira:

CódigoNomeSignificado
200OKA operação foi concluída com sucesso
201CreatedUm recurso foi criado
204No ContentA operação foi concluída sem corpo na resposta
301Moved PermanentlyO recurso possui um novo endereço permanente
302FoundO recurso está temporariamente em outro endereço
400Bad RequestA requisição não pôde ser processada por um problema do cliente
401UnauthorizedA requisição exige credenciais de autenticação válidas
403ForbiddenO servidor recusou o acesso ao recurso
404Not FoundO recurso solicitado não foi encontrado
405Method Not AllowedO método HTTP não é aceito pelo recurso
429Too Many RequestsO cliente enviou requisições acima do limite permitido
500Internal Server ErrorO servidor encontrou um erro interno
503Service UnavailableO serviço não está disponível no momento

O pacote net/http possui constantes para representar esses códigos, como http.StatusOK, http.StatusNotFound e http.StatusInternalServerError. Essas constantes permitem trabalhar com nomes que identificam o significado do código, evitando espalhar valores numéricos diretamente pelo programa. Também facilitam a leitura das condições responsáveis por interpretar as respostas.


Podemos verificar o campo StatusCode para identificar se a operação retornou um resultado esperado. Em uma ferramenta que consulta determinado endpoint, por exemplo, podemos considerar que qualquer código fora da faixa de sucesso precisa receber tratamento adicional.

// Verifica se o servidor retornou um status de sucesso.
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf(
"servidor retornou HTTP %d",
resp.StatusCode,
)
}

Esse trecho pressupõe que resp representa uma resposta HTTP válida e que o código está dentro de uma função que retorna error. A condição verifica se o status pertence à faixa de 200 até 299, que representa respostas de sucesso. Entretanto, nem todos esses códigos possuem o mesmo significado, por isso algumas aplicações precisam verificar um valor específico, como http.StatusOK ou http.StatusCreated.


Um cuidado é que client.Do não retorna automaticamente um erro quando o servidor responde com 404 ou 500. Esses códigos representam respostas HTTP válidas, mesmo que indiquem que a operação não foi concluída como esperado. Por isso, precisamos distinguir erros de transporte, que podem impedir o recebimento da resposta, dos códigos de status retornados pelo servidor.



Lendo o corpo com io.ReadAll​


O corpo de uma resposta HTTP pode conter texto, HTML, JSON ou dados binários. Para respostas pequenas, podemos usar io.ReadAll, que lê todos os bytes disponíveis até encontrar o fim do fluxo ou algum erro. Essa função retorna um slice de bytes contendo o conteúdo recebido, permitindo convertê-lo para string quando sabemos que a resposta representa texto.


Vamos criar um programa que consulta um endereço e apresenta o corpo retornado pelo servidor. Como não sabemos antecipadamente qual conteúdo será recebido, o exemplo apenas converte os bytes para string e os escreve na saída padrão. Essa abordagem é adequada para demonstrações com respostas pequenas, mas exige cuidados quando o tamanho do conteúdo não é conhecido.

package main

import (
"fmt"
"io"
"net/http"
"os"
"time"
)

func main() {
client := &http.Client{
Timeout: 5 * time.Second,
}

// Envia uma requisição HTTP.
resp, err := client.Get("https://example.com")

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

defer resp.Body.Close()

// Lê todo o conteúdo do corpo da resposta.
dados, err := io.ReadAll(resp.Body)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao ler resposta:",
err,
)

return
}

// Apresenta o status e o conteúdo recebido.
fmt.Println("Status:", resp.Status)
fmt.Println(string(dados))
}

A função io.ReadAll recebe um valor que implementa io.Reader e continua lendo os dados até alcançar o fim do fluxo. O resultado é armazenado na variável dados, que possui o tipo []byte. Depois, usamos string(dados) para representar esses bytes como uma string, permitindo apresentar o conteúdo diretamente no terminal.


O problema dessa abordagem é que io.ReadAll tenta armazenar todo o conteúdo recebido na memória. Se o servidor devolver uma resposta muito grande ou enviar dados continuamente, o programa poderá consumir uma quantidade excessiva de memória. Por isso, em ferramentas que consultam servidores externos, normalmente precisamos limitar a quantidade de dados recebidos ou processar a resposta de maneira gradual.


Também precisamos considerar que o corpo nem sempre contém texto. Uma resposta pode transportar imagens, arquivos compactados ou outros formatos binários, e converter esses dados diretamente para string não significa que serão apresentados corretamente. Antes de interpretar o conteúdo, podemos consultar o header Content-Type, que informa o formato declarado pelo servidor.



Limitando o tamanho da resposta​


Quando consultamos servidores externos, não devemos assumir que todas as respostas terão um tamanho pequeno. Um servidor pode devolver um arquivo maior do que o esperado ou transmitir dados continuamente, mantendo o cliente ocupado e consumindo memória. Para reduzir esse risco, podemos limitar a quantidade de bytes que serão lidos do corpo.


O pacote io fornece a função io.LimitReader, que cria um leitor responsável por permitir a leitura de uma quantidade máxima de bytes. Podemos combinar esse recurso com io.ReadAll, impedindo que o programa armazene uma resposta inteira quando seu tamanho ultrapassa o limite definido.


Para identificar se a resposta excede o tamanho permitido, podemos configurar a leitura para aceitar um byte além do limite. Se a quantidade recebida ultrapassar o máximo, saberemos que o corpo contém mais dados do que a aplicação aceita.

// Define um limite de 1 MiB para o conteúdo.
const tamanhoMaximo = 1 << 20

// Permite ler um byte além do limite para detectar excesso.
leitor := io.LimitReader(
resp.Body,
tamanhoMaximo+1,
)

// Lê somente a quantidade permitida.
dados, err := io.ReadAll(leitor)

if err != nil {
return fmt.Errorf(
"ler resposta: %w",
err,
)
}

// Rejeita respostas maiores do que o limite.
if len(dados) > tamanhoMaximo {
return errors.New("resposta excedeu o tamanho permitido")
}

Esse trecho pressupõe que resp contém uma resposta HTTP válida, que os pacotes io, fmt e errors foram importados e que o código está dentro de uma função que retorna error. O valor 1 << 20 representa 1048576 bytes, equivalente a 1 MiB. A leitura de um byte adicional permite identificar que o servidor enviou mais conteúdo do que o limite estabelecido.


A função io.LimitReader limita apenas a quantidade de bytes que serão entregues ao código responsável pela leitura. Ela não encerra automaticamente a conexão nem impede que o servidor tenha enviado outros dados pela rede. Quando decidimos abandonar uma resposta por excesso de tamanho, normalmente devemos fechar seu corpo, sabendo que a conexão poderá não ser reaproveitada pelo transporte.


Também podemos consultar resp.ContentLength, que representa o tamanho declarado do corpo quando essa informação está disponível. Entretanto, esse valor pode ser -1 quando o tamanho não é conhecido, além de depender das informações fornecidas pelo servidor. Por isso, não devemos depender exclusivamente dele para limitar a quantidade de dados realmente recebidos.



Lendo headers da resposta​


Assim como uma requisição, uma resposta HTTP também possui headers. Esses campos podem informar o tipo de conteúdo, características de cache, informações sobre redirecionamentos e outros dados relacionados à resposta. Em Go, eles ficam disponíveis no campo resp.Header, que possui o tipo http.Header.


Podemos consultar um header usando o método Get, que recebe o nome do campo e retorna seu primeiro valor associado. Se o header não existir, o resultado será uma string vazia. Como os nomes dos headers não diferenciam letras maiúsculas e minúsculas, podemos consultá-los usando a grafia mais conveniente.

// Consulta alguns headers da resposta HTTP.
tipo := resp.Header.Get("Content-Type")
servidor := resp.Header.Get("Server")

fmt.Println("Content-Type:", tipo)
fmt.Println("Server:", servidor)

Esse trecho pressupõe que resp contém uma resposta válida e que o pacote fmt foi importado. O header Content-Type informa o tipo de conteúdo declarado pelo servidor, enquanto Server pode identificar o software responsável pelo serviço. Entretanto, esse segundo campo é opcional, pode ser omitido ou modificado e não deve ser tratado como uma identificação confiável da tecnologia usada.


Quando um header possui vários valores, podemos usar Values para consultar todos eles. Também precisamos considerar que alguns headers possuem regras próprias para combinar valores, por isso nem sempre podemos interpretar uma lista de strings como se todos os elementos fossem equivalentes. O comportamento correto depende da definição de cada campo HTTP.



Interpretando respostas JSON​


APIs HTTP frequentemente retornam dados no formato JSON, permitindo que diferentes aplicações troquem informações estruturadas. Em Go, podemos interpretar esse conteúdo usando o pacote encoding/json, que oferece a função json.Unmarshal e o tipo json.Decoder. O decoder permite interpretar diretamente um fluxo de dados, sem precisar armazenar todo o corpo em um slice de bytes antes da conversão.


Vamos considerar que um servidor possui um endpoint /status, responsável por retornar um objeto JSON com o estado da aplicação. Podemos criar uma estrutura Go correspondente ao conteúdo esperado e usar json.NewDecoder para preencher seus campos.

{
"status": "ok"
}

O programa seguinte consulta esse endpoint e interpreta o conteúdo recebido. Como o endereço é local, precisamos ter um servidor escutando na porta 8080 e disponibilizando a rota /status. Esse servidor será implementado na seção de criação de endpoints.

package main

import (
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)

type Status struct {
Status string `json:"status"`
}

func main() {
client := &http.Client{
Timeout: 5 * time.Second,
}

// Consulta o endpoint responsável pelo status.
resp, err := client.Get(
"http://127.0.0.1:8080/status",
)

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

defer resp.Body.Close()

// Verifica se a requisição foi aceita.
if resp.StatusCode != http.StatusOK {
fmt.Fprintln(
os.Stderr,
"Status inesperado:",
resp.Status,
)

return
}

var resultado Status

// Interpreta o JSON diretamente do corpo recebido.
decoder := json.NewDecoder(resp.Body)

if err := decoder.Decode(&resultado); err != nil {
fmt.Fprintln(
os.Stderr,
"Erro ao interpretar JSON:",
err,
)

return
}

fmt.Println("Estado:", resultado.Status)
}

O tipo Status representa a estrutura esperada para a resposta. A tag json:"status" permite associar o campo JSON ao campo exportado da estrutura Go, enquanto json.NewDecoder cria um objeto responsável por interpretar o fluxo recebido. A chamada Decode(&resultado) lê um valor JSON e tenta preencher a estrutura com as informações encontradas.


A verificação de resp.StatusCode acontece antes da interpretação porque o servidor pode devolver uma mensagem de erro com outro formato. Se tentássemos interpretar qualquer resposta como JSON, poderíamos receber um erro de conversão quando o conteúdo fosse HTML ou texto simples. Mesmo quando o código indica sucesso, a aplicação ainda precisa verificar se os dados recebidos atendem às regras esperadas.


O método Decode interpreta um valor JSON por chamada. Isso significa que uma chamada bem-sucedida não garante, sozinha, que não existam outros valores ou dados adicionais depois do objeto. Quando precisamos validar rigorosamente o conteúdo recebido, podemos executar outra leitura e verificar se o decoder encontra io.EOF, indicando que não existem novos valores no fluxo.


Também precisamos lembrar que o pacote encoding/json não valida automaticamente todas as regras de negócio. Um campo ausente pode deixar a variável com seu valor zero, enquanto uma string vazia pode ser aceita normalmente. Por isso, depois da conversão, ainda podemos precisar verificar se os campos obrigatórios estão preenchidos e se seus valores são adequados para a operação.



Criando um servidor HTTP simples​


Um servidor HTTP é um programa que recebe requisições, identifica a operação solicitada e devolve respostas seguindo as regras do protocolo. Diferentemente de um cliente, que inicia a comunicação, o servidor disponibiliza um endereço e uma porta para receber solicitações de outras aplicações. Em Go, podemos criar esse comportamento usando o mesmo pacote net/http empregado para enviar requisições.


O pacote fornece recursos para receber conexões, interpretar mensagens HTTP e encaminhar as requisições para funções responsáveis por atendê-las. Isso evita precisar implementar manualmente a leitura dos headers, a interpretação dos métodos e a construção básica das respostas. O servidor continua sendo responsável por definir quais rotas existem e como cada requisição deverá ser processada.


Uma rota representa um caminho associado a determinada operação da aplicação, enquanto um handler é a função ou objeto responsável por processar a requisição recebida. Podemos associar diferentes rotas a handlers específicos, permitindo que o mesmo servidor ofereça várias funcionalidades. O pacote net/http fornece o tipo ServeMux para organizar esse encaminhamento.



Criando o primeiro servidor HTTP​


Podemos criar um servidor usando http.NewServeMux, que retorna um roteador responsável por associar padrões de requisição aos handlers cadastrados. Depois, usamos HandleFunc para registrar uma função que será executada quando uma requisição corresponder ao padrão informado. Essa função recebe um http.ResponseWriter e um ponteiro para http.Request.


O http.ResponseWriter permite construir a resposta que será enviada ao cliente, enquanto http.Request contém as informações recebidas, como método, URL, headers e corpo. Podemos usar esses objetos para consultar os dados da requisição e devolver uma resposta adequada, sem precisar trabalhar diretamente com os bytes do protocolo HTTP.

package main

import (
"fmt"
"log"
"net/http"
"time"
)

func main() {
// Cria um roteador HTTP.
mux := http.NewServeMux()

// Registra o endpoint de saudação.
mux.HandleFunc("GET /saudacao", func(
w http.ResponseWriter,
r *http.Request,
) {
// Informa o formato do conteúdo retornado.
w.Header().Set(
"Content-Type",
"text/plain; charset=utf-8",
)

// Escreve a resposta enviada ao cliente.
fmt.Fprintln(w, "Olá, HTTP em Go!")
})

// Configura o servidor HTTP.
servidor := &http.Server{
Addr: "127.0.0.1:8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
}

log.Println("Servidor HTTP em 127.0.0.1:8080")

// Inicia o recebimento de requisições.
if err := servidor.ListenAndServe(); err != nil {
log.Println("Servidor encerrado:", err)
}
}

A função http.NewServeMux cria um roteador independente, enquanto mux.HandleFunc registra o handler responsável pela rota /saudacao. Nesse exemplo, usamos o padrão "GET /saudacao", que associa o endpoint ao método GET. Esse formato de roteamento por método faz parte das melhorias introduzidas no Go 1.22, portanto o exemplo exige essa versão ou uma posterior.


Dentro do handler, w.Header().Set define o header Content-Type, indicando que a resposta contém texto codificado em UTF-8. Depois, fmt.Fprintln escreve a mensagem no ResponseWriter, fazendo com que o conteúdo seja enviado ao cliente. Como não definimos explicitamente um código de status, o servidor retorna 200 OK quando a primeira escrita é realizada com sucesso.


A estrutura http.Server permite configurar o endereço, o handler e outros parâmetros relacionados ao funcionamento do servidor. Nesse exemplo, usamos ReadHeaderTimeout para limitar o tempo permitido para o recebimento dos headers das requisições. O método ListenAndServe inicia o servidor e permanece executando até que aconteça um erro ou que seu encerramento seja solicitado.


O endereço 127.0.0.1:8080 permite receber requisições pela interface local, evitando disponibilizar o serviço diretamente em outras interfaces de rede. Isso é conveniente para desenvolvimento e testes, principalmente quando o servidor ainda não possui autenticação ou outros mecanismos de controle. Para permitir conexões externas, precisaríamos escolher outro endereço de escuta e considerar as regras de segurança do ambiente.



Testando o servidor HTTP​


Depois de iniciar o servidor, podemos acessar seu endpoint por um navegador ou por uma ferramenta de linha de comando. Como a rota foi registrada para receber requisições GET, basta enviar uma solicitação para o caminho /saudacao. Podemos usar curl, que permite realizar requisições HTTP diretamente pelo terminal e apresentar o conteúdo retornado.

curl http://127.0.0.1:8080/saudacao

A resposta será semelhante a esta:

Olá, HTTP em Go!

O comando curl envia uma requisição GET para o endereço informado e apresenta o corpo da resposta. Como o servidor devolve texto simples, o conteúdo aparece diretamente no terminal. Se quisermos consultar também o código de status e os headers recebidos, podemos acrescentar a opção -i, que apresenta essas informações junto ao corpo.

curl -i http://127.0.0.1:8080/saudacao

Teremos uma resposta semelhante a esta:

HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Date: ...
Content-Length: 17

Olá, HTTP em Go!

O campo Date representa a data informada pelo servidor, enquanto Content-Length indica o tamanho do corpo em bytes quando esse header está presente. Esses valores podem variar conforme a implementação e as condições da resposta. O status 200 OK indica que o servidor processou normalmente a requisição e devolveu o conteúdo definido pelo handler.


Se tentarmos acessar uma rota que não foi registrada, o roteador normalmente responderá com 404 Not Found. Se acessarmos /saudacao usando um método não aceito pelo padrão, o ServeMux também poderá responder com 405 Method Not Allowed, indicando que o caminho existe, mas não aceita aquele método. Esses comportamentos são administrados automaticamente pelo roteador quando usamos os padrões de métodos disponíveis a partir do Go 1.22.



Entendendo ResponseWriter e Request​


Toda função registrada como handler recebe dois argumentos responsáveis por representar os lados da comunicação HTTP. O primeiro é http.ResponseWriter, usado para construir e enviar a resposta, enquanto o segundo é *http.Request, que contém as informações recebidas do cliente. Essa separação permite consultar os dados da requisição e definir a resposta sem trabalhar diretamente com sockets TCP.


O objeto http.Request possui campos como Method, URL, Header, Body e RemoteAddr. Esses campos permitem identificar o método solicitado, consultar o endereço, acessar os headers, ler o corpo e obter informações sobre o ponto remoto da comunicação. Nem todos precisam ser consultados em cada handler, porque isso depende da operação implementada.

func informacoes(
w http.ResponseWriter,
r *http.Request,
) {
// Consulta as informações recebidas do cliente.
metodo := r.Method
caminho := r.URL.Path
endereco := r.RemoteAddr

// Define o formato da resposta.
w.Header().Set(
"Content-Type",
"text/plain; charset=utf-8",
)

// Apresenta as informações consultadas.
fmt.Fprintf(
w,
"Método: %s\nCaminho: %s\nCliente: %s\n",
metodo,
caminho,
endereco,
)
}

Esse trecho pressupõe que os pacotes fmt e net/http foram importados. A função pode ser registrada como handler usando mux.HandleFunc, permitindo que seja executada quando uma requisição corresponder à rota escolhida. O campo r.RemoteAddr normalmente contém o endereço de rede e a porta associados ao peer conectado, mas não representa obrigatoriamente o usuário final quando existem proxies ou outros intermediários.


O objeto ResponseWriter também permite definir explicitamente o código de status usando WriteHeader. Quando precisamos retornar um código diferente de 200, normalmente devemos configurá-lo antes de escrever o corpo. Depois que a resposta começa a ser enviada, o código de status já pode ter sido transmitido, impedindo que seja alterado normalmente.

// Informa o formato da resposta.
w.Header().Set(
"Content-Type",
"text/plain; charset=utf-8",
)

// Define o código de status antes de escrever o corpo.
w.WriteHeader(http.StatusCreated)

// Envia o conteúdo da resposta.
fmt.Fprintln(w, "Recurso criado.")

Esse trecho pressupõe que w representa um http.ResponseWriter e que os pacotes fmt e net/http foram importados. A constante http.StatusCreated representa o código 201, usado quando uma operação cria um recurso. Se escrevêssemos o corpo antes de chamar WriteHeader, o servidor normalmente enviaria automaticamente 200 OK, impedindo que a chamada posterior modificasse esse status.


Também precisamos considerar que os handlers HTTP podem executar concorrentemente, porque o servidor atende diferentes conexões e requisições usando goroutines. Quando handlers acessam mapas, variáveis ou outras estruturas compartilhadas que podem ser modificadas, precisamos aplicar mecanismos de sincronização. Variáveis locais criadas dentro de cada chamada do handler normalmente não exigem esse cuidado quando não são compartilhadas com outras goroutines.



Criando endpoints para teste​


Um endpoint HTTP representa um ponto de acesso disponibilizado por uma aplicação para executar determinada operação. Ele normalmente é identificado pela combinação de um método HTTP e um caminho, permitindo que o servidor encaminhe as requisições para funções específicas. Podemos criar endpoints simples para testar clientes, validar mensagens e observar o comportamento de diferentes códigos de status.


Para organizar uma API de teste, podemos criar rotas responsáveis por consultar o estado do servidor, receber parâmetros, interpretar JSON e responder a requisições POST. Essas operações permitem trabalhar com diferentes partes do protocolo sem depender de serviços externos. Também podemos criar um endpoint que demora para responder, permitindo testar os mecanismos de timeout apresentados posteriormente.


Vamos usar o ServeMux do pacote net/http, com padrões que incluem métodos HTTP e parâmetros de caminho. Os padrões como "GET /usuarios/{id}" exigem Go 1.22 ou posterior, porque essa versão introduziu o suporte a métodos e segmentos nomeados diretamente no roteador da biblioteca padrão.



Criando endpoints GET​


Podemos criar vários endpoints GET registrando handlers diferentes no mesmo ServeMux. Cada rota representa uma operação específica e pode consultar informações presentes na URL antes de construir a resposta. Um endpoint /status, por exemplo, pode informar se o processo está respondendo, enquanto /consulta pode receber parâmetros enviados pela query string.


Para devolver respostas JSON, podemos usar json.NewEncoder, que converte uma estrutura Go para JSON e escreve o resultado diretamente no destino informado. Como http.ResponseWriter permite operações de escrita, ele pode ser passado ao encoder para construir a resposta. Antes disso, precisamos definir o header Content-Type, indicando que o corpo representa dados JSON.

func consultarStatus(
w http.ResponseWriter,
r *http.Request,
) {
// Define o formato da resposta.
w.Header().Set(
"Content-Type",
"application/json",
)

// Envia uma resposta JSON.
json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
})
}

func consultarParametros(
w http.ResponseWriter,
r *http.Request,
) {
// Obtém os parâmetros informados na URL.
nome := r.URL.Query().Get("nome")
limite := r.URL.Query().Get("limite")

// Retorna os valores recebidos.
w.Header().Set(
"Content-Type",
"application/json",
)

json.NewEncoder(w).Encode(map[string]string{
"nome": nome,
"limite": limite,
})
}

Essas funções pressupõem que os pacotes encoding/json e net/http foram importados. O método r.URL.Query() interpreta os parâmetros presentes na URL e retorna um conjunto de valores que pode ser consultado com Get. Quando o parâmetro não existe, Get retorna uma string vazia, permitindo que a aplicação decida se essa ausência é aceita ou representa um erro.


O método Encode converte os valores recebidos para JSON e escreve o resultado na resposta, acrescentando uma quebra de linha ao final. Essa operação também pode retornar um erro, que deve ser tratado quando precisamos identificar falhas durante a codificação ou o envio. Como a escrita pode começar durante a chamada, não devemos assumir que sempre será possível modificar o código de status depois de um erro.


Quando os parâmetros representam números ou outros tipos de dados, também precisamos convertê-los e validar seu conteúdo. No exemplo, limite foi mantido como string apenas para demonstrar a leitura da query string. Em uma aplicação que usa esse valor para controlar uma quantidade de registros, normalmente deveríamos convertê-lo usando strconv.Atoi e verificar se pertence ao intervalo permitido.



Criando endpoints com parâmetros no caminho​


Alguns endpoints recebem informações diretamente no caminho da URL, em vez de usar parâmetros de consulta. Uma API pode disponibilizar /usuarios/10, por exemplo, em que o número identifica o recurso solicitado. Para evitar registrar manualmente uma rota para cada identificador, podemos usar segmentos nomeados no padrão do ServeMux.


A partir do Go 1.22, podemos registrar um padrão como "GET /usuarios/{id}". Quando uma requisição corresponde a esse padrão, o valor do segmento identificado por id pode ser consultado usando o método r.PathValue. Isso permite separar a estrutura da rota do valor recebido em cada requisição.

func consultarUsuario(
w http.ResponseWriter,
r *http.Request,
) {
// Obtém o identificador presente no caminho.
idTexto := r.PathValue("id")

// Converte o identificador para número inteiro.
id, err := strconv.Atoi(idTexto)

if err != nil || id < 1 {
http.Error(
w,
"Identificador inválido.",
http.StatusBadRequest,
)

return
}

// Envia uma representação do recurso consultado.
w.Header().Set(
"Content-Type",
"application/json",
)

json.NewEncoder(w).Encode(map[string]any{
"id": id,
"nome": "Usuário de teste",
})
}

Esse trecho pressupõe que os pacotes encoding/json, net/http e strconv foram importados. O método r.PathValue("id") retorna o valor capturado pelo segmento nomeado da rota, enquanto strconv.Atoi tenta convertê-lo para número inteiro. Se a conversão falhar ou o identificador for menor que um, o servidor devolve 400 Bad Request, indicando que o parâmetro recebido não atende às regras esperadas.


Quando o identificador é válido, o handler retorna um objeto JSON com informações de teste. Nesse exemplo, os dados não são consultados em um banco, porque o objetivo é demonstrar o funcionamento dos parâmetros de caminho. Em uma API real, normalmente precisaríamos procurar o recurso solicitado e devolver 404 Not Found caso ele não existisse.


Também precisamos considerar que um segmento nomeado corresponde a apenas um segmento do caminho. Portanto, o padrão /usuarios/{id} não deve ser tratado como um mecanismo genérico para capturar todos os caminhos possíveis depois de /usuarios/. O roteador possui regras específicas para segmentos, métodos e padrões mais abrangentes, que devem ser consideradas quando criamos rotas mais complexas.



Criando endpoints POST com JSON​


Para receber JSON em um servidor HTTP, precisamos ler o corpo da requisição e converter seu conteúdo para uma estrutura Go. O pacote encoding/json oferece json.NewDecoder, que permite interpretar diretamente os dados recebidos de r.Body. Depois da conversão, podemos validar os campos e construir uma resposta que informe se a operação foi aceita.


Também precisamos limitar o tamanho do corpo recebido, porque um cliente pode enviar uma quantidade de dados muito maior do que aquela esperada pelo endpoint. O pacote net/http fornece http.MaxBytesReader, que permite restringir a quantidade de bytes que podem ser lidos do corpo da requisição. Esse mecanismo ajuda a controlar o consumo de memória e evita aceitar mensagens excessivamente grandes sem necessidade.


Vamos criar um endpoint /mensagens, responsável por receber um objeto JSON contendo um campo chamado texto. O servidor verificará se o conteúdo foi interpretado corretamente e devolverá uma resposta JSON confirmando o recebimento.

type Mensagem struct {
Texto string `json:"texto"`
}

func receberMensagem(
w http.ResponseWriter,
r *http.Request,
) {
// Limita o corpo da requisição a 1 MiB.
r.Body = http.MaxBytesReader(
w,
r.Body,
1<<20,
)

// Cria um decoder para interpretar o JSON.
decoder := json.NewDecoder(r.Body)

// Rejeita campos que não existem na estrutura.
decoder.DisallowUnknownFields()

var mensagem Mensagem

// Converte o corpo para a estrutura esperada.
if err := decoder.Decode(&mensagem); err != nil {
http.Error(
w,
"JSON inválido.",
http.StatusBadRequest,
)

return
}

// Verifica se existem dados adicionais.
var extra any

if err := decoder.Decode(&extra); err != io.EOF {
http.Error(
w,
"Conteúdo JSON adicional.",
http.StatusBadRequest,
)

return
}

// Valida o campo obrigatório.
if mensagem.Texto == "" {
http.Error(
w,
"O campo texto é obrigatório.",
http.StatusBadRequest,
)

return
}

// Retorna uma confirmação em JSON.
w.Header().Set(
"Content-Type",
"application/json",
)

json.NewEncoder(w).Encode(map[string]any{
"recebido": true,
"texto": mensagem.Texto,
})
}

Esse trecho pressupõe que os pacotes encoding/json, io e net/http foram importados. A função http.MaxBytesReader substitui o leitor do corpo por outro que impõe um limite de tamanho, enquanto json.NewDecoder interpreta os dados recebidos. O método DisallowUnknownFields faz com que campos desconhecidos provoquem um erro quando estamos convertendo um objeto JSON para uma estrutura Go.


A segunda chamada de Decode verifica se existem outros valores depois do primeiro objeto JSON. Como esperamos apenas um objeto por requisição, qualquer conteúdo adicional será rejeitado. O resultado io.EOF indica que o decoder alcançou o fim do corpo sem encontrar outro valor JSON, permitindo continuar a validação da mensagem recebida.


Depois da interpretação, verificamos se o campo Texto está preenchido. Se a validação falhar, usamos http.Error, que escreve uma resposta de erro com o código informado e uma mensagem textual. Essa função é conveniente para erros simples, mas APIs que seguem um formato JSON padronizado normalmente precisam construir suas próprias respostas de erro.


O exemplo usa 400 Bad Request para diferentes problemas relacionados ao corpo. Entretanto, também poderíamos diferenciar condições como tamanho excedido, usando 413 Content Too Large, ou conteúdo com tipo não aceito, usando 415 Unsupported Media Type. Para isso, precisaríamos examinar os erros retornados pelo decoder e verificar o header Content-Type antes de interpretar os dados.



Criando uma API HTTP para testes​


Podemos reunir os endpoints anteriores em um único programa, criando uma API local que permite testar diferentes métodos, parâmetros e tipos de resposta. Essa organização facilita os exemplos porque o mesmo servidor poderá ser consultado pelos clientes HTTP apresentados anteriormente. Também adicionaremos um endpoint /lento, que demora alguns segundos para responder e será usado nos testes de timeout.


Para manter o programa organizado, vamos separar a configuração das rotas da função responsável por enviar JSON. A função responderJSON receberá o conteúdo e o código de status, convertendo os dados antes de escrever a resposta. Isso permite evitar a repetição do mesmo tratamento de headers e status em todos os endpoints.

package main

import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strconv"
"time"
)

type Mensagem struct {
Texto string `json:"texto"`
}

func responderJSON(
w http.ResponseWriter,
status int,
valor any,
) {
// Converte a resposta antes de enviar os headers.
dados, err := json.Marshal(valor)

if err != nil {
http.Error(
w,
"Erro ao gerar resposta.",
http.StatusInternalServerError,
)

return
}

// Define o formato e o código de status.
w.Header().Set(
"Content-Type",
"application/json",
)

w.WriteHeader(status)

// Envia o JSON seguido por uma quebra de linha.
dados = append(dados, '\n')

if _, err := w.Write(dados); err != nil {
log.Println("Erro ao escrever resposta:", err)
}
}

func main() {
mux := http.NewServeMux()

// Endpoint para consultar o estado do servidor.
mux.HandleFunc("GET /status", func(
w http.ResponseWriter,
r *http.Request,
) {
responderJSON(
w,
http.StatusOK,
map[string]string{
"status": "ok",
},
)
})

// Endpoint que recebe parâmetros na URL.
mux.HandleFunc("GET /consulta", func(
w http.ResponseWriter,
r *http.Request,
) {
nome := r.URL.Query().Get("nome")
limiteTexto := r.URL.Query().Get("limite")

limite, err := strconv.Atoi(limiteTexto)

if err != nil || limite < 1 || limite > 100 {
http.Error(
w,
"Limite deve estar entre 1 e 100.",
http.StatusBadRequest,
)

return
}

responderJSON(
w,
http.StatusOK,
map[string]any{
"nome": nome,
"limite": limite,
},
)
})

// Endpoint que recebe um parâmetro no caminho.
mux.HandleFunc("GET /usuarios/{id}", func(
w http.ResponseWriter,
r *http.Request,
) {
id, err := strconv.Atoi(r.PathValue("id"))

if err != nil || id < 1 {
http.Error(
w,
"Identificador inválido.",
http.StatusBadRequest,
)

return
}

responderJSON(
w,
http.StatusOK,
map[string]any{
"id": id,
"nome": "Usuário de teste",
},
)
})

// Endpoint que recebe um objeto JSON.
mux.HandleFunc("POST /mensagens", func(
w http.ResponseWriter,
r *http.Request,
) {
// Limita o tamanho do conteúdo recebido.
r.Body = http.MaxBytesReader(
w,
r.Body,
1<<20,
)

decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()

var mensagem Mensagem

if err := decoder.Decode(&mensagem); err != nil {
http.Error(
w,
"JSON inválido.",
http.StatusBadRequest,
)

return
}

// Impede múltiplos valores JSON no corpo.
var extra any

if err := decoder.Decode(&extra); err != io.EOF {
http.Error(
w,
"Conteúdo JSON adicional.",
http.StatusBadRequest,
)

return
}

if mensagem.Texto == "" {
http.Error(
w,
"O campo texto é obrigatório.",
http.StatusBadRequest,
)

return
}

responderJSON(
w,
http.StatusOK,
map[string]any{
"recebido": true,
"texto": mensagem.Texto,
},
)
})

// Endpoint que recebe campos de formulário.
mux.HandleFunc("POST /formulario", func(
w http.ResponseWriter,
r *http.Request,
) {
// Limita a leitura dos dados do formulário.
r.Body = http.MaxBytesReader(
w,
r.Body,
4096,
)

if err := r.ParseForm(); err != nil {
http.Error(
w,
"Formulário inválido.",
http.StatusBadRequest,
)

return
}

responderJSON(
w,
http.StatusOK,
map[string]string{
"nome": r.PostForm.Get("nome"),
"email": r.PostForm.Get("email"),
},
)
})

// Endpoint usado para testar timeouts.
mux.HandleFunc("GET /lento", func(
w http.ResponseWriter,
r *http.Request,
) {
select {
case <-time.After(3 * time.Second):
responderJSON(
w,
http.StatusOK,
map[string]string{
"resultado": "concluído",
},
)

case <-r.Context().Done():
// Interrompe o atendimento após cancelamento.
return
}
})

// Configura o servidor HTTP local.
servidor := &http.Server{
Addr: "127.0.0.1:8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 30 * time.Second,
}

log.Println("Servidor HTTP em 127.0.0.1:8080")

// Inicia o servidor.
if err := servidor.ListenAndServe(); err != nil {
log.Println("Servidor encerrado:", err)
}
}

O programa cria um ServeMux e registra os endpoints usando padrões que combinam métodos HTTP e caminhos. Cada handler fica responsável por interpretar os parâmetros necessários e construir a resposta correspondente. Quando a requisição não corresponde a nenhuma rota, o roteador devolve uma resposta de recurso não encontrado, enquanto métodos incompatíveis com uma rota existente podem provocar 405 Method Not Allowed.


A função responderJSON recebe um valor Go e o converte para JSON usando json.Marshal. Essa conversão acontece antes de chamar WriteHeader, permitindo detectar problemas de serialização enquanto ainda é possível retornar um erro HTTP. Depois disso, o programa define o tipo de conteúdo, escreve o status e envia os bytes produzidos.


O endpoint /consulta recebe os parâmetros nome e limite, convertendo o segundo para número inteiro e verificando se pertence ao intervalo permitido. Já /usuarios/{id} usa PathValue para obter o identificador presente no caminho. Essas rotas demonstram duas maneiras diferentes de transportar parâmetros em requisições GET, permitindo escolher o formato mais adequado para cada operação.


O endpoint /mensagens recebe JSON e valida seu conteúdo antes de responder, enquanto /formulario recebe dados codificados como formulário. Nesse segundo caso, usamos r.ParseForm para interpretar os campos e r.PostForm para consultar especificamente os valores enviados no corpo. Isso evita misturar esses dados com parâmetros de consulta que também poderiam estar presentes na URL.


O endpoint /lento aguarda três segundos antes de devolver sua resposta. Entretanto, ele também observa r.Context().Done(), que permite identificar determinadas situações em que a requisição foi cancelada ou o cliente deixou de aguardar. Isso evita continuar executando a espera quando o atendimento não precisa mais produzir uma resposta para aquela requisição.


A estrutura http.Server define limites de tempo para algumas etapas da comunicação. O campo ReadHeaderTimeout controla o recebimento dos headers, ReadTimeout estabelece um prazo para ler a requisição completa, WriteTimeout controla o prazo de escrita da resposta e IdleTimeout limita o tempo de espera de conexões ociosas mantidas para reaproveitamento. Esses limites ajudam a evitar que operações permaneçam aguardando indefinidamente.


Esse servidor foi desenvolvido para testes locais e não possui autenticação, armazenamento persistente ou mecanismos completos de proteção. Os dados retornados são definidos diretamente pelo programa, permitindo observar o comportamento do HTTP sem depender de um banco de dados. Para disponibilizar uma API em ambientes externos, precisaríamos considerar outros aspectos, como TLS, controle de acesso, limites de requisições e tratamento de erros adequado às necessidades da aplicação.



Testando os endpoints pelo terminal​


Depois de iniciar o servidor, podemos acessar seus endpoints usando curl. Cada comando permite verificar uma parte da comunicação HTTP, incluindo os métodos aceitos, os parâmetros enviados e o conteúdo das respostas. Como o servidor escuta na interface local, os exemplos podem ser executados na mesma máquina em que o programa Go está rodando.


Para consultar o estado da aplicação, enviamos uma requisição GET para /status.

curl -i http://127.0.0.1:8080/status

A resposta terá um corpo semelhante a este:

{
"status": "ok"
}

Para testar os parâmetros de consulta, podemos enviar nome e limite diretamente pela URL. Nesse caso, usamos aspas para impedir que o shell interprete o caractere & como parte de sua própria sintaxe de execução.

curl -i "http://127.0.0.1:8080/consulta?nome=go&limite=10"

O corpo retornado será equivalente a este:

{
"nome": "go",
"limite": 10
}

O endpoint /usuarios/{id} permite testar parâmetros presentes diretamente no caminho. Podemos informar um identificador numérico e verificar se o servidor consegue interpretá-lo corretamente antes de devolver a representação do recurso solicitado.

curl -i http://127.0.0.1:8080/usuarios/10

A resposta terá um conteúdo semelhante a este:

{
"id": 10,
"nome": "Usuário de teste"
}

Para enviar JSON usando POST, precisamos informar o método, o header Content-Type e o conteúdo que será transmitido. A opção -d do curl permite enviar dados no corpo e faz com que a requisição use POST quando nenhum método alternativo foi definido explicitamente.

curl -i \
-X POST \
-H "Content-Type: application/json" \
-d '{"texto":"Olá servidor"}' \
http://127.0.0.1:8080/mensagens

O servidor interpretará o objeto recebido e devolverá uma confirmação semelhante a esta:

{
"recebido": true,
"texto": "Olá servidor"
}

Também podemos testar o envio de formulários usando --data-urlencode, que codifica os campos antes de enviá-los. Esse formato é diferente do JSON porque representa os valores seguindo as regras de formulários codificados em URLs, permitindo que o servidor os interprete com r.ParseForm.

curl -i \
--data-urlencode "nome=Go" \
--data-urlencode "email=teste@example.com" \
http://127.0.0.1:8080/formulario

A resposta será equivalente a esta:

{
"nome": "Go",
"email": "teste@example.com"
}

Esses testes permitem verificar se o servidor está interpretando corretamente os métodos, parâmetros e corpos recebidos. Também podemos enviar valores inválidos para observar os códigos de erro produzidos pelos handlers, como um identificador não numérico ou um JSON sem o campo obrigatório. Isso ajuda a verificar se a aplicação está rejeitando informações incompatíveis com as regras definidas.



Testando endpoints automaticamente com httptest​


Além de executar requisições manualmente, podemos testar handlers HTTP usando o pacote net/http/httptest, que faz parte da biblioteca padrão. Esse pacote fornece recursos para simular requisições e registrar respostas, permitindo verificar o comportamento de um endpoint sem precisar iniciar um servidor TCP real. Isso facilita a criação de testes automatizados para as regras implementadas pelos handlers.


O tipo ResponseRecorder funciona como um destino de resposta que armazena as informações produzidas pelo handler. Podemos combiná-lo com httptest.NewRequest, que cria uma requisição de entrada apropriada para testes. Depois, chamamos diretamente o handler e verificamos o código de status e o conteúdo retornado.


Vamos criar um teste simples para um endpoint que devolve 200 OK e a mensagem pong. O arquivo de teste deve possuir o sufixo _test.go, permitindo que seja identificado pelo comando go test.

package main

import (
"net/http"
"net/http/httptest"
"testing"
)

func TestPing(t *testing.T) {
// Cria uma requisição HTTP de teste.
req := httptest.NewRequest(
http.MethodGet,
"/ping",
nil,
)

// Cria um registrador para armazenar a resposta.
recorder := httptest.NewRecorder()

// Define o handler que será testado.
handler := http.HandlerFunc(func(
w http.ResponseWriter,
r *http.Request,
) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("pong"))
})

// Executa o handler sem abrir uma porta de rede.
handler.ServeHTTP(recorder, req)

// Verifica o código retornado.
if recorder.Code != http.StatusOK {
t.Fatalf(
"status inesperado: %d",
recorder.Code,
)
}

// Verifica o conteúdo da resposta.
if recorder.Body.String() != "pong" {
t.Fatalf(
"resposta inesperada: %q",
recorder.Body.String(),
)
}
}

A função httptest.NewRequest cria um objeto que representa uma requisição recebida pelo servidor. O ResponseRecorder armazena os headers, o status e os bytes escritos pelo handler, permitindo consultar esses valores depois da execução. Como chamamos ServeHTTP diretamente, não precisamos estabelecer uma conexão TCP ou reservar uma porta para executar esse teste.


O pacote testing fornece o tipo testing.T, usado para registrar falhas e controlar a execução dos testes. O método Fatalf apresenta uma mensagem e interrompe o teste atual quando uma condição esperada não é atendida. Nesse exemplo, verificamos se o handler retornou 200 OK e se o corpo contém exatamente a mensagem definida.


Podemos executar esse teste usando o comando go test, dentro de um projeto Go configurado para permitir a execução de testes. Quando precisamos testar a comunicação HTTP completa, incluindo cliente e servidor, também podemos usar httptest.NewServer, que inicia um servidor HTTP de teste em um endereço local escolhido automaticamente. Essa abordagem permite testar requisições reais sem depender de portas fixas.



Trabalhando com timeout em requisições HTTP​


Requisições HTTP dependem de diferentes operações que podem consumir tempo, incluindo resolução DNS, estabelecimento da conexão, negociação TLS, envio dos dados e recebimento da resposta. Se alguma dessas etapas demorar mais do que o esperado, o programa poderá permanecer aguardando e atrasar outras operações. Por isso, normalmente precisamos estabelecer limites de tempo quando desenvolvemos clientes HTTP.


Em Go, podemos configurar timeouts diretamente no http.Client ou usar o pacote context para controlar uma requisição específica. Esses mecanismos possuem responsabilidades relacionadas, mas não são exatamente equivalentes. O timeout do cliente estabelece um limite para as requisições executadas por ele, enquanto um contexto permite controlar o prazo e o cancelamento de determinada operação.


Também existem configurações específicas relacionadas ao transporte HTTP, que permitem limitar etapas individuais da comunicação. Entretanto, não precisamos começar por essas opções para implementar um cliente simples. Normalmente é melhor definir primeiro um limite geral adequado e depois considerar configurações mais específicas quando o comportamento da aplicação exigir.



Definindo timeout no http.Client​


O campo Timeout de http.Client representa o tempo máximo permitido para uma requisição executada por aquele cliente. Esse limite inclui o tempo de conexão, possíveis redirecionamentos e a leitura do corpo da resposta. O temporizador continua ativo mesmo depois que métodos como Get ou Do retornam, permitindo interromper a leitura do corpo caso o prazo total seja ultrapassado.


Podemos configurar um cliente com timeout de dois segundos e enviar uma requisição para o endpoint /lento, criado anteriormente. Como esse endpoint demora três segundos para responder, esperamos que a operação seja interrompida antes de receber uma resposta completa.

package main

import (
"fmt"
"net/http"
"os"
"time"
)

func main() {
// Cria um cliente com timeout de dois segundos.
client := &http.Client{
Timeout: 2 * time.Second,
}

// Consulta um endpoint que demora três segundos.
resp, err := client.Get(
"http://127.0.0.1:8080/lento",
)

if err != nil {
fmt.Fprintln(
os.Stderr,
"Erro na requisição:",
err,
)

return
}

defer resp.Body.Close()

fmt.Println("Status:", resp.Status)
}

Nesse exemplo, o cliente estabelece um limite de dois segundos para a requisição, enquanto o servidor demora três segundos antes de produzir sua resposta. Quando o tempo máximo é ultrapassado, o cliente retorna um erro relacionado ao timeout, permitindo que o programa interrompa a operação. O erro normalmente contém informações sobre a requisição e o motivo do encerramento.


Se aumentarmos o timeout para cinco segundos, a requisição poderá ser concluída normalmente, desde que o servidor responda dentro desse intervalo e não aconteçam outras falhas. Isso demonstra que o valor precisa ser escolhido conforme o comportamento esperado da operação, considerando o tempo necessário para estabelecer a conexão e receber os dados.


O valor zero em http.Client.Timeout significa que o cliente não possui um limite total configurado por esse campo. Isso não impede que determinadas operações falhem por outros motivos ou que existam timeouts definidos em componentes do transporte. Entretanto, não devemos depender de limites implícitos quando a aplicação precisa controlar quanto tempo pode aguardar uma resposta.



Cancelando requisições com context​


O pacote context permite associar um prazo ou sinal de cancelamento a uma requisição HTTP específica. Podemos usar context.WithTimeout para criar um contexto que será cancelado depois de determinado intervalo e passá-lo para http.NewRequestWithContext. Dessa maneira, a requisição fica associada ao contexto desde sua criação, permitindo controlar seu ciclo de vida.


O contexto de uma requisição enviada por um cliente controla diferentes etapas da comunicação, incluindo a obtenção de uma conexão, o envio da requisição, o recebimento dos headers e a leitura do corpo. Quando o contexto é cancelado, as operações que respeitam esse sinal podem retornar um erro, permitindo interromper o trabalho que deixou de ser necessário.


Vamos criar uma requisição com um limite de dois segundos, usando um cliente que possui timeout global de cinco segundos. Nesse caso, o contexto possui o prazo mais curto e deverá interromper a operação primeiro, desde que o endpoint demore mais do que esse intervalo.

package main

import (
"context"
"errors"
"fmt"
"net/http"
"os"
"time"
)

func main() {
// Cria um contexto com limite de dois segundos.
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)

defer cancel()

// Cria a requisição associada ao contexto.
req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"http://127.0.0.1:8080/lento",
nil,
)

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

// Define um limite geral para o cliente.
client := &http.Client{
Timeout: 5 * time.Second,
}

// Executa a requisição.
resp, err := client.Do(req)

if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Tempo limite da requisição excedido.")
return
}

fmt.Fprintln(
os.Stderr,
"Erro HTTP:",
err,
)

return
}

defer resp.Body.Close()

fmt.Println("Status:", resp.Status)
}

A função context.WithTimeout cria um contexto derivado com prazo de dois segundos e retorna uma função de cancelamento. Essa função é registrada com defer cancel(), garantindo que os recursos associados ao contexto sejam liberados quando a função terminar. Mesmo quando o timeout acontece automaticamente, devemos chamar cancel para evitar manter recursos desnecessários.


A função http.NewRequestWithContext recebe o contexto como primeiro argumento, seguido pelo método, pela URL e pelo corpo da requisição. Isso associa a operação ao prazo definido anteriormente, permitindo que client.Do reconheça o cancelamento durante a comunicação. Quando o contexto expira, o erro pode ser identificado usando errors.Is com context.DeadlineExceeded.


O exemplo também configura um timeout de cinco segundos no cliente. Como o contexto possui um prazo mais curto, ele normalmente será responsável por interromper a requisição. Se o cliente tivesse um prazo menor, esse outro limite poderia encerrar a operação primeiro. Quando precisamos distinguir essas situações, podemos examinar o erro retornado e consultar o estado do contexto.


O cancelamento do contexto também não significa que o servidor remoto interromperá obrigatoriamente todo o processamento. O cliente deixa de aguardar a operação e o servidor pode receber um sinal de cancelamento, mas precisa observar esse sinal para interromper suas próprias tarefas. Por isso, handlers que executam operações demoradas normalmente devem consultar r.Context() ou passar esse contexto para funções que oferecem suporte a cancelamento.



Identificando erros de timeout​


Quando uma requisição falha, o erro retornado pode representar diferentes condições. Uma falha de resolução DNS, uma conexão recusada e um timeout não possuem o mesmo significado, embora todas possam impedir o recebimento de uma resposta. Para identificar especificamente condições relacionadas ao tempo, podemos consultar os valores definidos pelo pacote context ou verificar se o erro implementa a interface net.Error.


A interface net.Error possui o método Timeout, que permite identificar se determinado erro representa uma condição de timeout. Podemos usar errors.As para verificar se o erro retornado implementa essa interface, mesmo quando foi encapsulado por outras operações. Isso é conveniente porque o pacote net/http pode retornar erros que incluem informações adicionais sobre a requisição executada.

func verificarTimeout(err error) bool {
if err == nil {
return false
}

// Identifica um prazo excedido pelo contexto.
if errors.Is(err, context.DeadlineExceeded) {
return true
}

var erroRede net.Error

// Verifica se o erro representa um timeout de rede.
if errors.As(err, &erroRede) {
return erroRede.Timeout()
}

return false
}

Esse trecho pressupõe que os pacotes context, errors e net foram importados. A função recebe um valor do tipo error e verifica primeiro se ele corresponde ao prazo excedido de um contexto. Depois, usa errors.As para identificar erros que implementam net.Error, consultando o método Timeout quando essa condição é atendida.


Esse mecanismo permite diferenciar falhas por tempo excedido de outros problemas, mas não informa automaticamente qual etapa da comunicação consumiu todo o prazo. Para diagnósticos mais detalhados, podemos precisar de mecanismos adicionais que acompanhem as etapas da requisição. O pacote net/http/httptrace, por exemplo, fornece recursos para observar eventos relacionados à execução de requisições HTTP.


Também precisamos evitar repetir automaticamente todas as requisições que falham por timeout. Uma requisição POST pode ter sido recebida e processada pelo servidor antes que o cliente perdesse a capacidade de receber sua resposta. Se repetirmos essa operação sem verificar as regras da API, poderemos executar a mesma ação mais de uma vez, provocando resultados inesperados.


Para operações que podem ser repetidas, precisamos considerar a semântica do método HTTP e o comportamento específico do serviço. Algumas APIs fornecem mecanismos como chaves de idempotência, que permitem identificar requisições repetidas e reduzir o risco de executar novamente uma operação que já foi processada. Esse comportamento depende da implementação da API e não é fornecido automaticamente pelo cliente HTTP do Go.



Timeouts específicos do transporte HTTP​


Além do timeout geral de http.Client, podemos configurar limites relacionados a etapas específicas da comunicação usando http.Transport. Esse tipo é responsável pelo transporte das requisições HTTP e permite controlar aspectos como conexões ociosas, uso de proxies e determinados limites de tempo. Essas opções são úteis quando precisamos identificar ou controlar com mais precisão onde uma requisição pode permanecer aguardando.


Uma configuração possível é definir um timeout para estabelecer conexões TCP usando net.Dialer, além de um limite para aguardar os headers da resposta usando ResponseHeaderTimeout. Esses valores possuem responsabilidades diferentes, porque o primeiro controla a criação da conexão de rede, enquanto o segundo limita a espera pelos headers depois que a requisição foi enviada.

package main

import (
"fmt"
"net"
"net/http"
"os"
"time"
)

func main() {
// Configura o estabelecimento das conexões TCP.
dialer := &net.Dialer{
Timeout: 3 * time.Second,
}

// Configura o transporte das requisições HTTP.
transporte := &http.Transport{
DialContext: dialer.DialContext,
TLSHandshakeTimeout: 3 * time.Second,
ResponseHeaderTimeout: 5 * time.Second,
IdleConnTimeout: 30 * time.Second,
}

// Cria um cliente com limite geral de dez segundos.
client := &http.Client{
Transport: transporte,
Timeout: 10 * time.Second,
}

// Executa uma requisição usando o transporte configurado.
resp, err := client.Get("https://example.com")

if err != nil {
fmt.Fprintln(os.Stderr, err)
return
}

defer resp.Body.Close()

fmt.Println("Status:", resp.Status)
}

O objeto net.Dialer estabelece um limite de três segundos para a tentativa de conexão TCP. O campo DialContext do transporte recebe o método responsável por essa operação, permitindo que o cliente HTTP use o dialer configurado. Já TLSHandshakeTimeout limita o tempo da negociação TLS, enquanto ResponseHeaderTimeout controla a espera pelos headers da resposta depois que o envio da requisição foi concluído.


O campo IdleConnTimeout representa o tempo máximo em que uma conexão ociosa pode permanecer no conjunto de conexões disponíveis para reaproveitamento. Esse parâmetro não deve ser confundido com o tempo máximo de execução de uma requisição ativa. Já client.Timeout estabelece um limite geral de dez segundos, incluindo as diferentes etapas da comunicação e a leitura do corpo.


Precisamos considerar que o transporte pode reutilizar conexões já existentes. Quando isso acontece, uma nova requisição não precisa necessariamente estabelecer outra conexão TCP ou executar uma nova negociação TLS. Por isso, os limites associados a essas etapas somente terão efeito quando elas realmente acontecerem, enquanto o timeout geral continua controlando a duração da requisição.


Outro cuidado é que criar um http.Transport personalizado significa assumir a configuração de seus campos. Os valores que não forem definidos podem ter comportamentos diferentes daqueles presentes em http.DefaultTransport, que possui configurações padrão próprias. Quando precisamos modificar apenas alguns parâmetros e preservar os demais, podemos criar uma cópia usando http.DefaultTransport.(*http.Transport).Clone() e alterar somente os campos necessários.


Como clientes e transportes podem ser compartilhados por várias goroutines, normalmente devemos configurá-los antes de iniciar as requisições concorrentes. Depois disso, podemos reaproveitá-los para enviar diferentes solicitações sem modificar suas configurações durante o uso. Essa organização ajuda a evitar condições de corrida e permite aproveitar o gerenciamento de conexões oferecido pela biblioteca padrão.



Cancelamento no servidor HTTP​


O contexto também está disponível no lado do servidor, por meio do método r.Context(). Ele representa o contexto associado à requisição recebida e pode ser cancelado quando o cliente encerra a comunicação, quando a requisição é cancelada em determinadas condições ou quando o handler termina. Isso permite que funções executadas durante o atendimento reconheçam quando a operação deixou de ser necessária.


Em endpoints que executam tarefas demoradas, podemos usar select para aguardar o resultado da operação ou o cancelamento do contexto. Essa abordagem evita continuar trabalhando desnecessariamente quando não existe mais um cliente aguardando a resposta. Entretanto, as operações internas precisam respeitar o sinal de cancelamento para que o encerramento aconteça corretamente.


O endpoint /lento apresentado anteriormente já usa esse mecanismo. Podemos observar sua parte principal no trecho seguinte:

select {
case <-time.After(3 * time.Second):
// Envia a resposta depois do intervalo esperado.
responderJSON(
w,
http.StatusOK,
map[string]string{
"resultado": "concluído",
},
)

case <-r.Context().Done():
// Interrompe o atendimento quando a requisição é cancelada.
return
}

O select aguarda até que o intervalo de três segundos termine ou que o contexto da requisição seja cancelado. Se o prazo terminar primeiro, o handler envia a resposta normalmente. Se o contexto for cancelado antes, a função retorna sem tentar escrever o resultado, permitindo que os recursos associados ao atendimento sejam liberados.


Esse comportamento também demonstra por que timeouts do cliente e do servidor precisam ser tratados separadamente. O cliente pode decidir quanto tempo aceita aguardar uma resposta, enquanto o servidor precisa controlar suas próprias operações e reconhecer cancelamentos quando possível. Um limite definido de um lado da comunicação não substitui automaticamente os mecanismos de controle necessários no outro.



Cuidados com timeouts em servidores HTTP​


Servidores HTTP também precisam de limites de tempo para evitar conexões que permanecem abertas sem concluir suas operações. O campo ReadHeaderTimeout de http.Server controla quanto tempo o servidor permite para receber os headers da requisição. Isso ajuda a reduzir o impacto de clientes que estabelecem conexões e enviam os headers lentamente, mantendo recursos ocupados.


O campo ReadTimeout estabelece um prazo para a leitura da requisição completa, incluindo seu corpo, enquanto WriteTimeout controla o prazo relacionado à escrita da resposta. Já IdleTimeout limita o tempo de espera de conexões ociosas mantidas para receber novas requisições. Esses parâmetros possuem responsabilidades diferentes e precisam ser configurados conforme o comportamento esperado dos endpoints.


Não devemos aplicar os mesmos valores indiscriminadamente a todos os serviços. Um endpoint que recebe pequenos objetos JSON possui necessidades diferentes de outro que recebe arquivos grandes ou transmite respostas continuamente. Um timeout muito curto pode interromper operações legítimas, enquanto um limite excessivamente longo pode permitir que clientes mantenham conexões e goroutines ocupadas por mais tempo do que o necessário.


Também precisamos considerar que o encerramento do servidor pode acontecer enquanto existem requisições em andamento. O método http.Server.Shutdown permite iniciar um encerramento controlado, interrompendo o recebimento de novas conexões e aguardando que as requisições ativas terminem, respeitando o contexto informado. Para esse comportamento funcionar corretamente, o programa principal precisa aguardar a conclusão de Shutdown, em vez de encerrar imediatamente quando ListenAndServe retorna http.ErrServerClosed.


Por último, devemos diferenciar HTTP de HTTPS. Os exemplos locais usam HTTP porque o objetivo é demonstrar a comunicação sem precisar configurar certificados TLS. Quando o servidor transmite informações sensíveis ou precisa ser acessado por redes não confiáveis, normalmente devemos trabalhar com HTTPS e configurar corretamente os mecanismos de segurança. Timeouts ajudam a controlar recursos e falhas de comunicação, mas não substituem criptografia, autenticação ou validação dos dados recebidos.