Introdução ao Pacote net/http de Go
O pacote net/http é uma das bibliotecas padrão mais poderosas do Go, oferecendo suporte completo para construir servidores e clientes HTTP sem dependências externas. Diferentemente de outras linguagens que frequentemente dependem de frameworks pesados, Go fornece primitivas suficientemente baixo nível e bem projetadas para que você possa criar aplicações HTTP escaláveis e eficientes diretamente da stdlib.
A filosofia do Go nesse aspecto é clara: forneça ferramentas robustas, simples e compostas. O pacote net/http segue rigorosamente esse princípio. Neste artigo, você compreenderá como os componentes principais funcionam, desde a anatomia de um servidor HTTP até a execução de requisições de cliente, passando por conceitos como handlers, middlewares e tratamento de erros.
Fundamentos do Servidor HTTP
Conceito e Arquitetura Básica
Um servidor HTTP em Go é construído sobre o tipo http.Server, que encapsula toda a lógica necessária para escutar conexões TCP, interpretar requisições HTTP e enviar respostas. A forma mais simples de iniciar um servidor é usando http.ListenAndServe(), que combina criação, configuração e escuta em uma única chamada. Por baixo dos panos, Go cria uma goroutine para cada conexão cliente, permitindo que centenas de milhares de requisições simultâneas sejam processadas de forma eficiente.
O conceito fundamental é o http.Handler. Trata-se de uma interface simples com apenas um método: ServeHTTP(ResponseWriter, *Request). Qualquer tipo que implemente esse método pode processar requisições HTTP. Essa simplicidade é intencional: permite composição, reutilização e testes diretos sem abstração desnecessária.
package main
import (
"fmt"
"net/http"
)
// Handler simples que implementa a interface http.Handler
type HelloHandler struct{}
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Olá, %s!", r.URL.Query().Get("nome"))
}
func main() {
handler := &HelloHandler{}
http.ListenAndServe(":8080", handler)
}
Funções Convenientes e Roteamento Básico
Para a maioria dos casos reais, você não criará tipos personalizados que implementam http.Handler. Em vez disso, usará http.HandleFunc(), que converte uma função simples em um handler. Essa função recebe os mesmos parâmetros que ServeHTTP() e é transformada internamente em um tipo que implementa a interface.
package main
import (
"fmt"
"net/http"
)
func main() {
// Registra handlers diretamente com funções
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"mensagem":"Página inicial"}`)
})
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Método não permitido", http.StatusMethodNotAllowed)
return
}
fmt.Fprintf(w, `{"usuarios":["Alice","Bob"]}`)
})
fmt.Println("Servidor rodando em :8080")
http.ListenAndServe(":8080", nil)
}
Quando você passa nil como segundo argumento de ListenAndServe(), Go usa o DefaultServeMux, um multiplexador global que armazena todos os handlers registrados via http.HandleFunc() e http.Handle(). Para aplicações maiores, crie um ServeMux personalizado para evitar efeitos colaterais globais.
Criando Middlewares Robustos
Middlewares são funções que envolvem handlers, adicionando comportamentos como logging, autenticação, CORS ou compressão. A forma idiomática em Go é usar uma função que recebe um http.Handler e retorna outro http.Handler.
package main
import (
"fmt"
"log"
"net/http"
"time"
)
// Middleware de logging que registra informações sobre cada requisição
func logMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
inicio := time.Now()
log.Printf("[%s] %s %s", r.Method, r.RequestURI, r.RemoteAddr)
next.ServeHTTP(w, r)
duracao := time.Since(inicio)
log.Printf("Requisição processada em %v", duracao)
})
}
// Middleware de autenticação simples
func autenticacaoMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer secret-token-123" {
http.Error(w, "Não autorizado", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Função auxiliar para encadear middlewares
func comMiddlewares(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}
func main() {
mux := http.NewServeMux()
// Handler principal
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Dados sensíveis retornados com sucesso")
})
// Aplica middlewares na ordem desejada
mux.Handle("/dados", comMiddlewares(
handler,
logMiddleware,
autenticacaoMiddleware,
))
log.Fatal(http.ListenAndServe(":8080", mux))
}
Cliente HTTP em Go
Fazendo Requisições Básicas
O pacote net/http fornece o tipo http.Client, que representa um cliente HTTP reutilizável. A abordagem padrão é criar um cliente uma única vez e reutilizá-lo para múltiplas requisições, pois ele gerencia pool de conexões internamente, economizando recursos e melhorando performance.
package main
import (
"fmt"
"io"
"net/http"
)
func main() {
// Criar um cliente HTTP
client := &http.Client{}
// Fazer uma requisição GET simples
resp, err := client.Get("https://api.github.com/users/golang")
if err != nil {
fmt.Printf("Erro na requisição: %v\n", err)
return
}
defer resp.Body.Close()
// Ler o corpo da resposta
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Printf("Erro ao ler resposta: %v\n", err)
return
}
fmt.Printf("Status: %d\n", resp.StatusCode)
fmt.Printf("Headers: %v\n", resp.Header)
fmt.Printf("Body: %s\n", string(body))
}
Requisições Customizadas com http.Request
Para casos onde você precisa controlar detalhes como headers personalizados, método HTTP específico ou timeout, construa uma requisição manualmente usando http.NewRequest(). Isso oferece granularidade completa sobre o que é enviado ao servidor.
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
// Dados que serão enviados no corpo da requisição
dados := map[string]interface{}{
"titulo": "Novo Post",
"corpo": "Este é um exemplo de POST com JSON",
}
// Serializar para JSON
payload, _ := json.Marshal(dados)
// Criar requisição manualmente
req, err := http.NewRequest(
http.MethodPost,
"https://api.exemplo.com/posts",
bytes.NewBuffer(payload),
)
if err != nil {
fmt.Printf("Erro ao criar requisição: %v\n", err)
return
}
// Adicionar headers customizados
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer token-123")
req.Header.Set("User-Agent", "MeuApp/1.0")
// Executar a requisição
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Erro ao executar requisição: %v\n", err)
return
}
defer resp.Body.Close()
fmt.Printf("Status: %d\n", resp.StatusCode)
}
Tratamento de Timeouts e Resiliência
Em ambientes de produção, timeouts são essenciais para evitar que seu programa fica preso esperando por servidores lentos ou não responsivos. Configure timeouts no http.Client usando time.Duration.
package main
import (
"fmt"
"io"
"net/http"
"time"
)
func main() {
// Cliente com timeouts configurados
client := &http.Client{
Timeout: 5 * time.Second,
}
// Você também pode configurar timeouts específicos
client.Timeout = 0 // Desabilita timeout global
transport := &http.Transport{
DialTimeout: 3 * time.Second, // Timeout para conectar
TLSHandshakeTimeout: 2 * time.Second, // Timeout para handshake TLS
IdleConnTimeout: 30 * time.Second, // Timeout de inatividade
}
client.Transport = transport
// Usar o cliente com retry simples
maxRetries := 3
var resp *http.Response
var err error
for tentativa := 1; tentativa <= maxRetries; tentativa++ {
resp, err = client.Get("https://httpbin.org/delay/2")
if err == nil {
break
}
fmt.Printf("Tentativa %d falhou: %v\n", tentativa, err)
if tentativa < maxRetries {
time.Sleep(time.Second * time.Duration(tentativa))
}
}
if err != nil {
fmt.Printf("Todas as tentativas falharam: %v\n", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Printf("Resposta recebida: %d bytes\n", len(body))
}
Padrões Avançados e Boas Práticas
Context para Cancelamento e Deadlines
O pacote context é fundamental para gerenciar ciclos de vida de requisições em Go. Use-o para implementar cancelamento, deadlines e passar valores através da cadeia de funções de forma segura e explícita.
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func main() {
// Criar um contexto com deadline
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
// Criar requisição associada ao contexto
req, _ := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://httpbin.org/delay/5",
nil,
)
// Executar requisição
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Printf("Erro: %v\n", err) // Timeout será respeitado
return
}
defer resp.Body.Close()
io.ReadAll(resp.Body)
fmt.Println("Sucesso")
}
No lado do servidor, você também pode usar context para passar valores entre middlewares:
package main
import (
"context"
"fmt"
"net/http"
)
type ContextKey string
const UserIDKey ContextKey = "user_id"
// Middleware que extrai user_id do header e armazena no context
func extrairUserMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Header.Get("X-User-ID")
ctx := context.WithValue(r.Context(), UserIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func main() {
mux := http.NewServeMux()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Recuperar user_id do context
userID := r.Context().Value(UserIDKey).(string)
fmt.Fprintf(w, "User ID: %s", userID)
})
mux.Handle("/perfil", extrairUserMiddleware(handler))
http.ListenAndServe(":8080", mux)
}
Error Handling e Status Codes Apropriados
A função http.Error() é conveniente, mas em aplicações reais você frequentemente quer retornar JSON estruturado. Crie helpers que padronizem suas respostas de erro.
package main
import (
"encoding/json"
"fmt"
"net/http"
)
// Estrutura padrão para respostas de erro
type ErrorResponse struct {
Erro string `json:"erro"`
Codigo int `json:"codigo"`
Detalhe string `json:"detalhe,omitempty"`
}
// Helper para retornar erros em JSON
func responderErro(w http.ResponseWriter, statusCode int, mensagem string, detalhe string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
resposta := ErrorResponse{
Erro: mensagem,
Codigo: statusCode,
Detalhe: detalhe,
}
json.NewEncoder(w).Encode(resposta)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/recurso", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
responderErro(w, http.StatusMethodNotAllowed, "Método não permitido", "Apenas GET é aceito")
return
}
id := r.URL.Query().Get("id")
if id == "" {
responderErro(w, http.StatusBadRequest, "Parâmetro ausente", "Parâmetro 'id' é obrigatório")
return
}
// Simular busca de recurso
if id != "123" {
responderErro(w, http.StatusNotFound, "Recurso não encontrado", fmt.Sprintf("ID %s não existe", id))
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"id": id, "nome": "Recurso"})
})
http.ListenAndServe(":8080", mux)
}
Conclusão
Durante este artigo, você aprendeu três conceitos fundamentais que formam a base para trabalhar com HTTP em Go. Primeiro, compreendeu que um servidor HTTP em Go é construído sobre a elegante interface http.Handler, permitindo composição simples através de middlewares sem frameworks pesados. Segundo, viu que o cliente HTTP (http.Client) deve ser reutilizado e configurado com devida atenção a timeouts e resiliência. Terceiro, internalizou que padrões idiomáticos como context para cancelamento e estruturas de erro padronizadas não são luxos, mas necessidades reais em código de produção.
O pacote net/http de Go é deliberadamente minimalista, mas essa aparente simplicidade mascara um design profundo. Use-o sem medo de que você está reinventando a roda — o Go fez esse trabalho fundamental tão bem que raramente você precisará de dependências externas para HTTP. A chave está em entender seus primitivos e aprender a combiná-los efetivamente.