Artigo 28 — Criando um servidor HTTP com net/http
Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 5 — I/O, HTTP e APIs
O servidor HTTP que já vem na caixa
Uma das razões pelas quais Go se tornou a linguagem preferida para backends é a qualidade do suporte HTTP na biblioteca padrão. Sem instalar nenhuma dependência externa, é possível criar um servidor HTTP production-ready, com roteamento, middlewares, timeouts, TLS e suporte a HTTP/2.
O Go 1.22 trouxe melhorias significativas ao roteador padrão — suporte a métodos HTTP e parâmetros de caminho diretamente nos padrões de rota — reduzindo ainda mais a necessidade de bibliotecas externas para casos simples.
O mínimo funcional
Um servidor HTTP em Go começa com três conceitos: o Handler, o ServeMux e o Server:
package main
import (
"fmt"
"net/http"
)
func main() {
// Handler mais simples possível
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Olá, mundo!")
})
fmt.Println("Servidor em http://localhost:8080")
http.ListenAndServe(":8080", nil) // nil usa o DefaultServeMux
}
Embora funcional, esse código usa o DefaultServeMux global — um estado compartilhado que torna testes difíceis. A forma recomendada é criar um ServeMux explícito:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handlerRaiz)
mux.HandleFunc("/saude", handlerSaude)
servidor := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Println("Servidor iniciado em :8080")
if err := servidor.ListenAndServe(); err != nil {
log.Fatal(err)
}
}
func handlerRaiz(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "API funcionando")
}
func handlerSaude(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"status":"ok"}`)
}
Os timeouts são essenciais em produção. Sem eles, conexões lentas ou maliciosas podem consumir recursos indefinidamente.
A interface Handler
O coração do sistema HTTP do Go é a interface http.Handler:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Qualquer tipo que implemente ServeHTTP pode ser usado como handler. http.HandlerFunc é um adaptador que converte funções com a assinatura correta em handlers:
// HandlerFunc é definido na biblioteca padrão como:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
Isso permite criar handlers como structs com estado:
type HandlerProdutos struct {
servico *ServicoProdutos
logger *slog.Logger
}
func (h *HandlerProdutos) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
h.listar(w, r)
case http.MethodPost:
h.criar(w, r)
default:
http.Error(w, "método não permitido", http.StatusMethodNotAllowed)
}
}
Roteamento com Go 1.22
O Go 1.22 adicionou suporte a métodos HTTP e parâmetros de caminho diretamente nos padrões do ServeMux:
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
)
type Produto struct {
ID int `json:"id"`
Nome string `json:"nome"`
Preco float64 `json:"preco"`
}
var produtos = []Produto{
{ID: 1, Nome: "Teclado", Preco: 250.0},
{ID: 2, Nome: "Mouse", Preco: 120.0},
{ID: 3, Nome: "Monitor", Preco: 1500.0},
}
func main() {
mux := http.NewServeMux()
// Método + caminho — Go 1.22+
mux.HandleFunc("GET /produtos", listarProdutos)
mux.HandleFunc("POST /produtos", criarProduto)
mux.HandleFunc("GET /produtos/{id}", buscarProduto)
mux.HandleFunc("PUT /produtos/{id}", atualizarProduto)
mux.HandleFunc("DELETE /produtos/{id}", deletarProduto)
fmt.Println("API em :8080")
http.ListenAndServe(":8080", mux)
}
func listarProdutos(w http.ResponseWriter, r *http.Request) {
responderJSON(w, http.StatusOK, produtos)
}
func buscarProduto(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "id inválido", http.StatusBadRequest)
return
}
for _, p := range produtos {
if p.ID == id {
responderJSON(w, http.StatusOK, p)
return
}
}
http.Error(w, "produto não encontrado", http.StatusNotFound)
}
func criarProduto(w http.ResponseWriter, r *http.Request) {
var p Produto
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
http.Error(w, "corpo inválido: "+err.Error(), http.StatusBadRequest)
return
}
p.ID = len(produtos) + 1
produtos = append(produtos, p)
responderJSON(w, http.StatusCreated, p)
}
func atualizarProduto(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "id inválido", http.StatusBadRequest)
return
}
var atualizado Produto
if err := json.NewDecoder(r.Body).Decode(&atualizado); err != nil {
http.Error(w, "corpo inválido", http.StatusBadRequest)
return
}
for i, p := range produtos {
if p.ID == id {
atualizado.ID = id
produtos[i] = atualizado
responderJSON(w, http.StatusOK, atualizado)
return
}
}
http.Error(w, "produto não encontrado", http.StatusNotFound)
}
func deletarProduto(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "id inválido", http.StatusBadRequest)
return
}
for i, p := range produtos {
if p.ID == id {
produtos = append(produtos[:i], produtos[i+1:]...)
w.WriteHeader(http.StatusNoContent)
return
}
}
http.Error(w, "produto não encontrado", http.StatusNotFound)
}
func responderJSON(w http.ResponseWriter, status int, dados any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(dados)
}
Middlewares: comportamento transversal
Middleware é uma função que envolve um handler, adicionando comportamento antes e/ou depois de sua execução. O padrão idiomático em Go retorna um http.Handler:
package main
import (
"log/slog"
"net/http"
"os"
"time"
)
// Middleware de logging
func comLogging(logger *slog.Logger, prox http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
inicio := time.Now()
// Wrapper para capturar o status code
rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
prox.ServeHTTP(rw, r)
logger.Info("requisição",
"método", r.Method,
"caminho", r.URL.Path,
"status", rw.status,
"duração", time.Since(inicio),
"ip", r.RemoteAddr,
)
})
}
// Middleware de recuperação de pânico
func comRecuperacao(prox http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("pânico recuperado", "erro", err, "caminho", r.URL.Path)
http.Error(w, "erro interno do servidor", http.StatusInternalServerError)
}
}()
prox.ServeHTTP(w, r)
})
}
// Middleware de CORS
func comCORS(origemPermitida string, prox http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", origemPermitida)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
prox.ServeHTTP(w, r)
})
}
// Middleware de autenticação por token
func comAutenticacao(token string, prox http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth != "Bearer "+token {
http.Error(w, "não autorizado", http.StatusUnauthorized)
return
}
prox.ServeHTTP(w, r)
})
}
// responseWriter captura o status code
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(status int) {
rw.status = status
rw.ResponseWriter.WriteHeader(status)
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
mux := http.NewServeMux()
mux.HandleFunc("GET /api/dados", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"dados":"protegidos"}`))
})
// Encadeamento de middlewares — da camada mais externa para a mais interna
var handler http.Handler = mux
handler = comAutenticacao("meu-token-secreto", handler)
handler = comCORS("*", handler)
handler = comRecuperacao(handler)
handler = comLogging(logger, handler)
servidor := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
logger.Info("servidor iniciado", "addr", ":8080")
servidor.ListenAndServe()
}
Lendo parâmetros da requisição
func handlerBusca(w http.ResponseWriter, r *http.Request) {
// Query parameters: /busca?q=teclado&pagina=2&limite=10
q := r.URL.Query().Get("q")
pagina, _ := strconv.Atoi(r.URL.Query().Get("pagina"))
limite, _ := strconv.Atoi(r.URL.Query().Get("limite"))
if pagina <= 0 {
pagina = 1
}
if limite <= 0 || limite > 100 {
limite = 20
}
fmt.Fprintf(w, "busca: q=%s pagina=%d limite=%d", q, pagina, limite)
}
func handlerFormulario(w http.ResponseWriter, r *http.Request) {
// Form data (application/x-www-form-urlencoded)
if err := r.ParseForm(); err != nil {
http.Error(w, "formulário inválido", http.StatusBadRequest)
return
}
nome := r.FormValue("nome")
email := r.FormValue("email")
fmt.Fprintf(w, "nome=%s email=%s", nome, email)
}
func handlerHeaders(w http.ResponseWriter, r *http.Request) {
// Lendo headers
contentType := r.Header.Get("Content-Type")
userAgent := r.Header.Get("User-Agent")
token := r.Header.Get("Authorization")
// Escrevendo headers na resposta
w.Header().Set("X-Request-ID", "abc-123")
w.Header().Set("Cache-Control", "no-cache")
fmt.Fprintf(w, "content-type=%s user-agent=%s token=%s",
contentType, userAgent, token)
}
Servindo arquivos estáticos
func main() {
mux := http.NewServeMux()
// API
mux.HandleFunc("GET /api/versao", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"versao":"1.0.0"}`))
})
// Arquivos estáticos — serve o diretório "./public"
fs := http.FileServer(http.Dir("./public"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
http.ListenAndServe(":8080", mux)
}
Encerramento gracioso
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(100 * time.Millisecond) // simula processamento
w.Write([]byte("ok"))
})
servidor := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Inicia em goroutine separada
go func() {
slog.Info("servidor iniciado", "addr", ":8080")
if err := servidor.ListenAndServe(); err != http.ErrServerClosed {
slog.Error("erro no servidor", "err", err)
os.Exit(1)
}
}()
// Aguarda sinal de encerramento
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
slog.Info("encerrando servidor...")
ctx, cancelar := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelar()
if err := servidor.Shutdown(ctx); err != nil {
slog.Error("erro no shutdown", "err", err)
os.Exit(1)
}
slog.Info("servidor encerrado com sucesso")
}
Testando handlers HTTP
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestListarProdutos(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/produtos", nil)
rr := httptest.NewRecorder()
listarProdutos(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("status esperado %d, obtido %d", http.StatusOK, rr.Code)
}
var resultado []Produto
if err := json.NewDecoder(rr.Body).Decode(&resultado); err != nil {
t.Fatalf("erro ao decodificar resposta: %v", err)
}
if len(resultado) == 0 {
t.Error("esperava ao menos um produto")
}
}
func TestBuscarProdutoNaoEncontrado(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/produtos/999", nil)
req.SetPathValue("id", "999") // simula parâmetro de rota
rr := httptest.NewRecorder()
buscarProduto(rr, req)
if rr.Code != http.StatusNotFound {
t.Errorf("status esperado %d, obtido %d",
http.StatusNotFound, rr.Code)
}
}
O pacote httptest é fundamental para testes de handlers — permite criar requisições e respostas falsas sem iniciar um servidor real.
Resumo do que foi coberto
Este artigo apresentou o servidor HTTP do Go em profundidade: o mínimo funcional com http.Server e timeouts, a interface Handler, roteamento moderno com Go 1.22 incluindo métodos e parâmetros de caminho, middlewares de logging, recuperação de pânico, CORS e autenticação, leitura de query parameters, form data e headers, servindo arquivos estáticos, encerramento gracioso com Shutdown e testes de handlers com httptest. O próximo artigo explora roteamento avançado com bibliotecas externas.
Referências e leituras complementares
-
Documentação do pacote net/http — Referência completa do servidor e cliente HTTP. https://pkg.go.dev/net/http
-
Go Blog: HTTP/2 — Suporte nativo a HTTP/2 em servidores Go. https://go.dev/blog/h2push
-
Go 1.22 Release Notes — Enhanced routing — Detalhes sobre as melhorias de roteamento. https://go.dev/blog/routing-enhancements
-
Go by Example: HTTP Servers — Exemplos práticos de servidores HTTP. https://gobyexample.com/http-servers
-
Documentação httptest — Referência do pacote de testes HTTP. https://pkg.go.dev/net/http/httptest
-
Let's Encrypt com Go — Configurando TLS automático com certmagic ou autocert. https://pkg.go.dev/golang.org/x/crypto/acme/autocert
Próximo artigo: Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi**
you asked
Sim