ENTENDA A IDEIA
A ideia central
Imagine o balcão de uma oficina. O cliente entrega uma ordem de serviço com o que deseja; a pessoa no balcão lê o pedido e preenche uma resposta. Ela não dirige até a casa do cliente nem controla a rua. Seu trabalho começa quando recebe o pedido e termina quando registra o resultado. Um handler HTTP ocupa essa fronteira: recebe uma requisição e escreve uma resposta.
ENTENDA A IDEIA
Essa separação resolve um problema concreto. Se a regra de resposta depende de abrir uma porta TCP, escolher uma porta livre e fazer uma chamada pela rede, cada teste fica lento e sujeito ao ambiente. Em Go, o contrato central é a interface http.Handler, com o método ServeHTTP(http.ResponseWriter, *http.Request). Você pode executar esse contrato inteiramente em memória. O servidor de produção e o teste entregam os mesmos dois argumentos.
*http.Request descreve o que chegou: método, caminho, headers, corpo e contexto. http.ResponseWriter é o destino da resposta. Primeiro você ajusta headers; depois escolhe o status; por fim escreve o corpo. A primeira escrita do corpo envia implicitamente 200 OK quando nenhum status foi informado. Essa conveniência também cria uma armadilha: tentar mudar o status depois de escrever já é tarde.
ENTENDA A IDEIA
O handler não devolve uma resposta como valor. Ele produz efeitos no ResponseWriter. Em produção, esses efeitos viram bytes na conexão. Com httptest.NewRecorder, viram campos inspecionáveis: Code, Header() e Body. A mesma função fica útil no trabalho e verificável sem rede, sem espera e sem processo auxiliar.
Há três detalhes de protocolo que merecem uma inspeção mais lenta. O status representa o resultado da operação, não o estado geral do processo. Um endpoint de saúde pode usar 204 porque não precisa entregar representação; uma criação costuma usar 201; uma entrada inadequada usa 400. Escolher sempre 200 força todo consumidor a abrir o corpo para saber se a operação funcionou e piora métricas de erro.
Headers descrevem como interpretar ou tratar a resposta. Content-Type fala do corpo enviado; Allow informa quais métodos um recurso aceita. Eles não são comentários opcionais. Um navegador, proxy ou SDK pode decidir comportamento com base neles. No teste, afirmar headers impede uma regressão que não apareceria ao comparar somente texto.
O corpo é uma sequência de bytes. Converter string em []byte deixa essa fronteira explícita. Para texto pequeno, a alocação é aceitável; para respostas estruturadas, um encoder escreverá no writer. O importante agora é perceber que chamadas sucessivas a Write concatenam partes da mesma resposta, não criam respostas separadas.
Também há diferença entre função handler e valor handler. Uma função comum com a assinatura certa ganha o método ServeHTTP por meio de http.HandlerFunc. Esse adapter é um tipo da biblioteca, não geração mágica. Já um struct pode implementar ServeHTTP diretamente e guardar logger, store ou configuração. Comece com função; migre para struct quando dependências justificarem estado nomeado.
Ao revisar um handler no trabalho, siga a ordem: quais entradas ele lê, quais ramos podem responder, qual status e headers cada ramo fixa, e se todo ramo termina. Depois execute cada caso com recorder. Essa disciplina encontra status implícito e resposta duplicada antes de discutir abstrações.
Finalmente, não confunda teste em memória com teste falso. O mux e os handlers reais executam; só o transporte é removido. Para o contrato do handler, prefira httptest.NewRequest com NewRecorder: eles mostram status, headers e corpo com menos partes. Quando TLS, redirects, conexões ou outro comportamento do transporte forem o risco, use httptest.NewTestServer(t, handler). No Go 1.27, ele usa uma rede falsa em memória, fornece um client configurado e registra a limpeza no teste.
EXPERIMENTE
Experimente com o código
Antes de rodar, preveja o status e o corpo exibidos na linha de saída. Depois troque o método para POST: o resultado muda? Ainda não, pois o handler não consulta r.Method. Em seguida, adicione uma guarda que devolva 405 Method Not Allowed para qualquer método diferente de GET e inclua o header Allow: GET.
Faça uma terceira mudança: chame Write antes de WriteHeader(http.StatusCreated) e observe o status. Explique por que ele permanece 200. Depois restaure a ordem correta. Essa previsão treina uma regra que evita respostas contraditórias em APIs reais.
CÓDIGO ANOTADO
Código anotado
O programa monta um handler, cria uma requisição em memória e captura a resposta. http.HandlerFunc adapta uma função com a assinatura esperada para a interface http.Handler.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
) CÓDIGO ANOTADO
Headers precisam ser definidos antes de WriteHeader. O retorno de Write é tratado, ainda que o recorder não falhe, porque ignorar erros de escrita sem decisão explícita mascara desconexões em handlers reais.
func greetingHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("API pronta"))
} CÓDIGO ANOTADO
O teste de mesa não chama ListenAndServe. Ele constrói as duas extremidades do contrato e invoca ServeHTTP diretamente.
func main() {
request := httptest.NewRequest(http.MethodGet, "/", nil)
response := httptest.NewRecorder()
handler := http.HandlerFunc(greetingHandler)
handler.ServeHTTP(response, request) CÓDIGO ANOTADO
Depois da chamada, o recorder contém tudo que um cliente observaria. Result oferece também um http.Response, mas os campos diretos bastam neste primeiro diagnóstico.
fmt.Println(response.Code)
fmt.Println(response.Header().Get("Content-Type"))
fmt.Println(response.Body.String())
} PASSO A PASSO
Veja a ideia em movimento
Avance pelos passos e observe como o estado muda a cada decisão.
DESAFIO
Construa um handler configurável
Construa um handler configurável
Implemente GreetingHandler(message string) http.Handler. Cada chamada deve devolver um handler que responde a qualquer requisição com status 200, Content-Type igual a text/plain; charset=utf-8 e corpo exatamente igual a message. A entrada é uma string já validada; a saída é um valor que satisfaz http.Handler. Exemplo: mensagem bom dia produz corpo bom dia. Não acrescente newline e não use servidor de rede.
Dica 1
Comece pelo contrato observável e trate o caso de erro antes do sucesso.
Dica 2
Use a biblioteca padrão indicada na assinatura e devolva exatamente o valor pedido.
A solução aparece depois do acerto ou de 3 tentativas.
Solução e explicação
package main
import "net/http"
// GreetingHandler responde uma saudação em texto.
func GreetingHandler(message string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(message))
})
}
Ver harness de testes somente leitura
package main
import (
"encoding/json"
"fmt"
"net/http/httptest"
)
type caseResult struct {
Case int `json:"case"`
Pass bool `json:"pass"`
Input string `json:"input,omitempty"`
Want string `json:"want,omitempty"`
Got string `json:"got,omitempty"`
}
func emit(n int, input, want, got string, pass bool) {
line, _ := json.Marshal(caseResult{n, pass, input, want, got})
fmt.Printf("RESULT %s\n", line)
}
func main() {
for i, tc := range []struct{ method, message string }{{"GET", "bom dia"}, {"POST", "olá"}} {
r := httptest.NewRequest(tc.method, "/", nil)
w := httptest.NewRecorder()
GreetingHandler(tc.message).ServeHTTP(w, r)
got := fmt.Sprintf("%d|%s|%s", w.Code, w.Body.String(), w.Header().Get("Content-Type"))
want := fmt.Sprintf("200|%s|text/plain; charset=utf-8", tc.message)
emit(i+1, tc.method, want, got, got == want)
}
}
DESAFIO
Restrinja o endpoint de saúde
Restrinja o endpoint de saúde
Implemente HealthHandler() http.Handler. Para GET, responda 204 No Content e corpo vazio. Para qualquer outro método, defina Allow: GET, responda 405 Method Not Allowed e encerre o handler. A entrada é a requisição; a saída observável está no writer. Os casos usam GET e POST, mas a regra vale também para PUT e DELETE.
Dica 1
Comece pelo contrato observável e trate o caso de erro antes do sucesso.
Dica 2
Use a biblioteca padrão indicada na assinatura e devolva exatamente o valor pedido.
A solução aparece depois do acerto ou de 3 tentativas.
Solução e explicação
package main
import "net/http"
// HealthHandler aceita somente GET e informa disponibilidade.
func HealthHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", http.MethodGet)
http.Error(w, "método não permitido", http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusNoContent)
})
}
Ver harness de testes somente leitura
package main
import (
"encoding/json"
"fmt"
"net/http/httptest"
)
type caseResult struct {
Case int `json:"case"`
Pass bool `json:"pass"`
Input string `json:"input,omitempty"`
Want string `json:"want,omitempty"`
Got string `json:"got,omitempty"`
}
func emit(n int, input, want, got string, pass bool) {
line, _ := json.Marshal(caseResult{n, pass, input, want, got})
fmt.Printf("RESULT %s\n", line)
}
func main() {
for i, method := range []string{"GET", "POST"} {
r := httptest.NewRequest(method, "/health", nil)
w := httptest.NewRecorder()
HealthHandler().ServeHTTP(w, r)
wantCode := 204
if method == "POST" {
wantCode = 405
}
pass := w.Code == wantCode && (method == "GET" || w.Header().Get("Allow") == "GET")
emit(i+1, method, fmt.Sprint(wantCode), fmt.Sprint(w.Code), pass)
}
}
VOCÊ CHEGOU AO RESUMO
O que você leva desta aula
- Um handler recebe
*http.Requeste escreve nohttp.ResponseWriter. - Headers precedem status; a primeira escrita pode fixar 200 implicitamente.
httptest.NewRequesteNewRecorderexecutam o contrato sem rede.- Guardas de erro terminam com
returnpara não misturar respostas. - A sequência termina em Rotas e ServeMux com padrões do Go 1.22.