Muitos desenvolvedores Go acreditam que precisam de frameworks como Gin, Echo ou Fiber para construir APIs REST. A verdade e que, desde o Go 1.22, o pacote net/http da standard library tem tudo que voce precisa para criar APIs profissionais. Neste tutorial, vamos construir uma API REST completa – com routing por metodo HTTP, path parameters, middleware, JSON e logging estruturado – sem nenhuma dependencia externa.
Se voce prefere entender os conceitos antes de ver codigo, recomendo nossos glossarios sobre HTTP, handler, middleware e router.
O Novo ServeMux do Go 1.22+
Antes do Go 1.22, o http.ServeMux era extremamente basico: so fazia matching de prefixos de URL, sem suporte a metodos HTTP ou path parameters. Isso forcava o uso de routers de terceiros como gorilla/mux ou chi. Agora o cenario e completamente diferente.
Routing com Metodos HTTP
O novo ServeMux aceita metodos HTTP diretamente no pattern:
mux := http.NewServeMux()
// Cada verbo HTTP pode ser mapeado separadamente
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
Isso e exatamente o que frameworks como Gin fazem – mas sem nenhuma dependencia externa. O ServeMux nativo agora suporta patterns como GET /path, POST /path/{param} e ate wildcards com {rest...} para capturar segmentos restantes.
Path Parameters com PathValue
Para acessar parametros da URL, use r.PathValue():
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// id contem o valor capturado de /api/users/{id}
fmt.Fprintf(w, "Buscando usuario: %s", id)
}
Sem conversoes complicadas, sem parsing manual de URL. Simples e direto – do jeito que Go funciona.
Estrutura Completa da API
Vamos construir uma API de gerenciamento de tarefas (todo list) passo a passo. Primeiro, os tipos basicos:
package main
import (
"encoding/json"
"log/slog"
"net/http"
"os"
"sync"
"time"
)
type Task struct {
ID string `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
CreatedAt time.Time `json:"created_at"`
}
type TaskStore struct {
mu sync.RWMutex
tasks map[string]Task
}
func NewTaskStore() *TaskStore {
return &TaskStore{
tasks: make(map[string]Task),
}
}
Usamos sync.RWMutex para permitir leituras concorrentes e escrita segura – um pattern essencial quando voce tem multiplas goroutines acessando o mesmo recurso.
Handlers JSON Idiomaticos
Cada handler segue o mesmo pattern: decodificar request, executar logica, encodar response. Veja os handlers completos:
func (s *TaskStore) handleList(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
tasks := make([]Task, 0, len(s.tasks))
for _, t := range s.tasks {
tasks = append(tasks, t)
}
writeJSON(w, http.StatusOK, tasks)
}
func (s *TaskStore) handleCreate(w http.ResponseWriter, r *http.Request) {
var input struct {
Title string `json:"title"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
writeError(w, http.StatusBadRequest, "JSON invalido")
return
}
if input.Title == "" {
writeError(w, http.StatusBadRequest, "Titulo e obrigatorio")
return
}
task := Task{
ID: generateID(),
Title: input.Title,
Done: false,
CreatedAt: time.Now(),
}
s.mu.Lock()
s.tasks[task.ID] = task
s.mu.Unlock()
writeJSON(w, http.StatusCreated, task)
}
func (s *TaskStore) handleGet(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
s.mu.RLock()
task, ok := s.tasks[id]
s.mu.RUnlock()
if !ok {
writeError(w, http.StatusNotFound, "Tarefa nao encontrada")
return
}
writeJSON(w, http.StatusOK, task)
}
func (s *TaskStore) handleDelete(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.tasks[id]; !ok {
writeError(w, http.StatusNotFound, "Tarefa nao encontrada")
return
}
delete(s.tasks, id)
w.WriteHeader(http.StatusNoContent)
}
Funcoes Auxiliares para JSON
Centralizar a escrita de JSON evita duplicacao e garante headers corretos em toda a API:
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
func generateID() string {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
O pattern de writeJSON com any (alias para interface{}) e idiomatico em Go moderno. Para tratamento de erros mais robusto, voce pode criar tipos de erro customizados – veja nosso artigo sobre tratamento de erros em Go.
Middleware: Logging, CORS e Panic Recovery
Middleware em Go sao simplesmente funcoes que recebem um http.Handler e retornam outro http.Handler. Nenhuma magia, nenhum framework:
Logging com slog
O pacote slog, introduzido no Go 1.21, e perfeito para logging estruturado em APIs:
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Wrapper para capturar o status code
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapped, r)
slog.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", wrapped.statusCode,
"duration", time.Since(start),
"ip", r.RemoteAddr,
)
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
CORS Middleware
Para APIs que serao consumidas por frontends em outros dominios:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
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
}
next.ServeHTTP(w, r)
})
}
Panic Recovery
Evita que um panic em um handler derrube o servidor inteiro:
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
slog.Error("panic recuperado",
"error", err,
"path", r.URL.Path,
)
writeError(w, http.StatusInternalServerError, "Erro interno")
}
}()
next.ServeHTTP(w, r)
})
}
Compondo Tudo: A Funcao main
Agora juntamos todas as pecas:
func main() {
// Logger estruturado
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
store := NewTaskStore()
mux := http.NewServeMux()
// Rotas da API
mux.HandleFunc("GET /api/tasks", store.handleList)
mux.HandleFunc("POST /api/tasks", store.handleCreate)
mux.HandleFunc("GET /api/tasks/{id}", store.handleGet)
mux.HandleFunc("DELETE /api/tasks/{id}", store.handleDelete)
// Health check
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
// Compoe middleware (executa de fora para dentro)
var handler http.Handler = mux
handler = loggingMiddleware(handler)
handler = corsMiddleware(handler)
handler = recoveryMiddleware(handler)
addr := ":8080"
slog.Info("servidor iniciado", "addr", addr)
if err := http.ListenAndServe(addr, handler); err != nil {
slog.Error("erro ao iniciar servidor", "error", err)
os.Exit(1)
}
}
Note como middleware sao compostos: cada um envolve o anterior, criando uma cadeia de processamento. Esse pattern e identico ao que frameworks usam internamente – voce so esta vendo como funciona por baixo dos panos.
Quando Usar net/http vs Frameworks
A standard library e suficiente para a maioria das APIs. Use frameworks quando precisar de:
- Binding automatico de query params, form data e headers
- Validacao declarativa de payloads (tags em structs)
- Routing avancado como grupos de rotas com prefixos
Para APIs com menos de 50 endpoints, net/http puro com middleware customizado e mais que suficiente. E voce ganha: zero dependencias, build mais rapido, e total controle sobre cada aspecto do seu servidor.
Se voce quer explorar como essa API se comporta em producao, veja nossos artigos sobre deploy com Docker, observabilidade com OpenTelemetry e testes de integracao com Testcontainers. Para quem trabalha com APIs em outras linguagens, vale comparar como Python e Kotlin lidam com o mesmo desafio.
Perguntas Frequentes
Preciso do Go 1.22+ para usar path parameters no ServeMux?
Sim. O routing aprimorado com metodos HTTP e path parameters so esta disponivel a partir do Go 1.22. Alem disso, seu go.mod precisa declarar go 1.22 ou superior para ativar essa funcionalidade. Em versoes anteriores, voce precisaria de um router de terceiros como chi ou gorilla/mux.
O ServeMux do net/http e tao rapido quanto routers de terceiros?
Em benchmarks, o ServeMux nativo tem performance comparavel ao chi e apenas ligeiramente mais lento que o httprouter (usado pelo Gin). Para a grande maioria das aplicacoes, a diferenca e irrelevante – o gargalo vai estar no banco de dados ou em chamadas externas, nao no router. O context do request e passado da mesma forma em ambos os casos.
Como adiciono autenticacao JWT a essa API?
Crie um middleware que extrai o token do header Authorization, valida com uma biblioteca como golang-jwt/jwt e injeta os claims no context do request usando context.WithValue(). Temos um tutorial completo sobre autenticacao JWT em Go que complementa este artigo.
Posso usar esse pattern em producao?
Sim. O net/http e battle-tested em producao no Google, Cloudflare, Docker e milhares de empresas. O servidor HTTP do Go e robusto, seguro e eficiente. Para producao, adicione timeouts configurados no http.Server, graceful shutdown e middleware de rate limiting. Voce pode ver vagas que pedem exatamente esse conhecimento no mercado brasileiro.