Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi
Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 5 — I/O, HTTP e APIs
Quando a biblioteca padrão não é suficiente
O roteador do Go 1.22 resolveu as lacunas mais críticas do net/http — suporte a métodos HTTP e parâmetros de caminho. Para muitos projetos, ele é suficiente. Mas projetos maiores frequentemente precisam de recursos adicionais: agrupamento de rotas com prefixo comum, middlewares por grupo, validação de parâmetros via expressões regulares, suporte a subrouters e integração com sistemas de middleware mais sofisticados.
É nesse espaço que roteadores externos brilham. Os dois mais usados pela comunidade Go são o Gorilla Mux e o Chi. Cada um tem filosofia e casos de uso distintos.
Gorilla Mux vs Chi: a escolha certa
Gorilla Mux é o roteador externo mais antigo e reconhecido do ecossistema Go. Oferece roteamento poderoso com suporte a expressões regulares, restrições de esquema e host, e geração de URLs. Foi por muito tempo a escolha padrão da comunidade.
Chi é mais moderno, minimalista e completamente compatível com a interface http.Handler da biblioteca padrão. Usa context para passar valores entre middlewares e handlers, tem um sistema de middlewares mais elegante e é ligeiramente mais performático. Para projetos novos, Chi é geralmente a escolha preferida.
Instalando as dependências
# Chi
go get github.com/go-chi/chi/v5
# Gorilla Mux
go get github.com/gorilla/mux
Chi: roteamento idiomático e moderno
Conceitos fundamentais do Chi
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func main() {
r := chi.NewRouter()
// Middlewares globais — executam para todas as rotas
r.Use(middleware.RequestID) // injeta X-Request-ID
r.Use(middleware.RealIP) // usa X-Real-IP ou X-Forwarded-For
r.Use(middleware.Logger) // logging de requisições
r.Use(middleware.Recoverer) // recupera de pânicos
r.Use(middleware.Timeout(30 * time.Second)) // timeout por requisição
// Rota simples
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("API Go com Chi"))
})
// Health check
r.Get("/saude", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
// Agrupamento de rotas com prefixo
r.Route("/api/v1", func(r chi.Router) {
r.Route("/produtos", rotasProdutos)
r.Route("/usuarios", rotasUsuarios)
r.Route("/pedidos", rotasPedidos)
})
fmt.Println("Servidor em :8080")
http.ListenAndServe(":8080", r)
}
Parâmetros de URL e tipos
func rotasProdutos(r chi.Router) {
r.Get("/", listarProdutos)
r.Post("/", criarProduto)
// Parâmetros de caminho com {nome}
r.Route("/{id}", func(r chi.Router) {
r.Use(middlewareProduto) // middleware apenas para rotas com {id}
r.Get("/", buscarProduto)
r.Put("/", atualizarProduto)
r.Delete("/", deletarProduto)
})
}
func buscarProduto(w http.ResponseWriter, r *http.Request) {
// chi.URLParam extrai parâmetros de caminho
id := chi.URLParam(r, "id")
fmt.Fprintf(w, "produto: %s", id)
}
// Middleware específico para rotas de produto
func middlewareProduto(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
if id == "" {
http.Error(w, "id obrigatório", http.StatusBadRequest)
return
}
next.ServeHTTP(w, r)
})
}
API REST completa com Chi
package main
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"sync"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type Produto struct {
ID int `json:"id"`
Nome string `json:"nome"`
Preco float64 `json:"preco"`
Categoria string `json:"categoria"`
CriadoEm time.Time `json:"criado_em"`
}
type ErroResposta struct {
Codigo int `json:"codigo"`
Mensagem string `json:"mensagem"`
}
// Repositório em memória para o exemplo
type RepoProdutos struct {
mu sync.RWMutex
dados map[int]Produto
proximo int
}
func NovoRepoProdutos() *RepoProdutos {
r := &RepoProdutos{
dados: make(map[int]Produto),
proximo: 1,
}
// Dados iniciais
r.Salvar(Produto{Nome: "Teclado", Preco: 250.0, Categoria: "perifericos"})
r.Salvar(Produto{Nome: "Mouse", Preco: 120.0, Categoria: "perifericos"})
r.Salvar(Produto{Nome: "Monitor", Preco: 1500.0, Categoria: "monitores"})
return r
}
func (r *RepoProdutos) Listar() []Produto {
r.mu.RLock()
defer r.mu.RUnlock()
lista := make([]Produto, 0, len(r.dados))
for _, p := range r.dados {
lista = append(lista, p)
}
return lista
}
func (r *RepoProdutos) BuscarPorID(id int) (Produto, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
p, ok := r.dados[id]
return p, ok
}
func (r *RepoProdutos) Salvar(p Produto) Produto {
r.mu.Lock()
defer r.mu.Unlock()
p.ID = r.proximo
p.CriadoEm = time.Now()
r.dados[p.ID] = p
r.proximo++
return p
}
func (r *RepoProdutos) Atualizar(id int, p Produto) (Produto, bool) {
r.mu.Lock()
defer r.mu.Unlock()
if _, existe := r.dados[id]; !existe {
return Produto{}, false
}
p.ID = id
r.dados[id] = p
return p, true
}
func (r *RepoProdutos) Deletar(id int) bool {
r.mu.Lock()
defer r.mu.Unlock()
if _, existe := r.dados[id]; !existe {
return false
}
delete(r.dados, id)
return true
}
// Handler com dependências injetadas via closure
type HandlerProdutos struct {
repo *RepoProdutos
}
func (h *HandlerProdutos) Rotas() chi.Router {
r := chi.NewRouter()
r.Get("/", h.Listar)
r.Post("/", h.Criar)
r.Route("/{id}", func(r chi.Router) {
r.Get("/", h.Buscar)
r.Put("/", h.Atualizar)
r.Delete("/", h.Deletar)
})
return r
}
func (h *HandlerProdutos) Listar(w http.ResponseWriter, r *http.Request) {
// Filtro por categoria via query param
categoria := r.URL.Query().Get("categoria")
produtos := h.repo.Listar()
if categoria != "" {
filtrados := produtos[:0]
for _, p := range produtos {
if p.Categoria == categoria {
filtrados = append(filtrados, p)
}
}
produtos = filtrados
}
responderJSON(w, http.StatusOK, produtos)
}
func (h *HandlerProdutos) Buscar(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
responderErro(w, http.StatusBadRequest, "id inválido")
return
}
produto, existe := h.repo.BuscarPorID(id)
if !existe {
responderErro(w, http.StatusNotFound, "produto não encontrado")
return
}
responderJSON(w, http.StatusOK, produto)
}
func (h *HandlerProdutos) Criar(w http.ResponseWriter, r *http.Request) {
var p Produto
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
responderErro(w, http.StatusBadRequest, "corpo inválido: "+err.Error())
return
}
if p.Nome == "" {
responderErro(w, http.StatusUnprocessableEntity, "nome é obrigatório")
return
}
if p.Preco <= 0 {
responderErro(w, http.StatusUnprocessableEntity, "preço deve ser positivo")
return
}
criado := h.repo.Salvar(p)
w.Header().Set("Location", fmt.Sprintf("/api/v1/produtos/%d", criado.ID))
responderJSON(w, http.StatusCreated, criado)
}
func (h *HandlerProdutos) Atualizar(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
responderErro(w, http.StatusBadRequest, "id inválido")
return
}
var p Produto
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
responderErro(w, http.StatusBadRequest, "corpo inválido")
return
}
atualizado, existe := h.repo.Atualizar(id, p)
if !existe {
responderErro(w, http.StatusNotFound, "produto não encontrado")
return
}
responderJSON(w, http.StatusOK, atualizado)
}
func (h *HandlerProdutos) Deletar(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(chi.URLParam(r, "id"))
if err != nil {
responderErro(w, http.StatusBadRequest, "id inválido")
return
}
if !h.repo.Deletar(id) {
responderErro(w, http.StatusNotFound, "produto não encontrado")
return
}
w.WriteHeader(http.StatusNoContent)
}
func responderJSON(w http.ResponseWriter, status int, dados any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(dados)
}
func responderErro(w http.ResponseWriter, status int, mensagem string) {
responderJSON(w, status, ErroResposta{Codigo: status, Mensagem: mensagem})
}
func main() {
repo := NovoRepoProdutos()
handlerProdutos := &HandlerProdutos{repo: repo}
r := chi.NewRouter()
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.Timeout(30 * time.Second))
r.Mount("/api/v1/produtos", handlerProdutos.Rotas())
fmt.Println("API em :8080")
http.ListenAndServe(":8080", r)
}
Gorilla Mux: poder e expressões regulares
Para projetos que precisam de roteamento mais sofisticado — validação de parâmetros via regex, restrições por host ou esquema, geração de URLs — o Gorilla Mux oferece recursos únicos:
package main
import (
"fmt"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
// Parâmetros com validação via regex
r.HandleFunc("/produtos/{id:[0-9]+}", buscarProduto).Methods("GET")
r.HandleFunc("/usuarios/{slug:[a-z0-9-]+}", buscarUsuario).Methods("GET")
r.HandleFunc("/arquivos/{caminho:.+}", servirArquivo).Methods("GET")
// Restrições por host
api := r.Host("api.exemplo.com").Subrouter()
api.HandleFunc("/v1/dados", handlerDados).Methods("GET")
// Restrições por esquema
seguro := r.Schemes("https").Subrouter()
seguro.HandleFunc("/admin", handlerAdmin).Methods("GET")
// Prefixo
v1 := r.PathPrefix("/api/v1").Subrouter()
v1.HandleFunc("/itens", listarItens).Methods("GET")
v1.HandleFunc("/itens/{id:[0-9]+}", buscarItem).Methods("GET", "PUT", "DELETE")
// Rota não encontrada personalizada
r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "rota não encontrada", http.StatusNotFound)
})
// Método não permitido personalizado
r.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "método não permitido", http.StatusMethodNotAllowed)
})
fmt.Println("Servidor com Gorilla Mux em :8080")
http.ListenAndServe(":8080", r)
}
func buscarProduto(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) // extrai parâmetros de caminho
id := vars["id"]
fmt.Fprintf(w, "produto: %s", id)
}
func buscarUsuario(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
slug := vars["slug"]
fmt.Fprintf(w, "usuário: %s", slug)
}
func servirArquivo(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
caminho := vars["caminho"]
fmt.Fprintf(w, "arquivo: %s", caminho)
}
func handlerDados(w http.ResponseWriter, r *http.Request) { w.Write([]byte("dados")) }
func handlerAdmin(w http.ResponseWriter, r *http.Request) { w.Write([]byte("admin")) }
func listarItens(w http.ResponseWriter, r *http.Request) { w.Write([]byte("itens")) }
func buscarItem(w http.ResponseWriter, r *http.Request) { w.Write([]byte("item")) }
Geração de URLs com Gorilla Mux
Um recurso exclusivo do Gorilla Mux é a geração de URLs a partir de nomes de rotas — útil para evitar URLs hardcoded no código:
r := mux.NewRouter()
r.HandleFunc("/produtos/{id:[0-9]+}", buscarProduto).
Methods("GET").
Name("produto.buscar")
r.HandleFunc("/usuarios/{slug}", buscarUsuario).
Methods("GET").
Name("usuario.buscar")
// Gerando URLs
url, err := r.Get("produto.buscar").URL("id", "42")
if err == nil {
fmt.Println(url.String()) // /produtos/42
}
url, err = r.Get("usuario.buscar").URL("slug", "ricardo-matos")
if err == nil {
fmt.Println(url.String()) // /usuarios/ricardo-matos
}
Middlewares customizados com Chi
Chi facilita a criação de middlewares que passam dados via context:
package main
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
)
// Chave de contexto com tipo próprio
type chaveCtx string
const chaveUsuarioAutenticado chaveCtx = "usuario"
type UsuarioAutenticado struct {
ID int
Nome string
Papel string
}
// Middleware de autenticação JWT simulado
func middlewareJWT(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "token obrigatório", http.StatusUnauthorized)
return
}
// Simulação de validação de JWT
if token != "Bearer token-valido" {
http.Error(w, "token inválido", http.StatusUnauthorized)
return
}
usuario := UsuarioAutenticado{ID: 1, Nome: "Ricardo", Papel: "admin"}
ctx := context.WithValue(r.Context(), chaveUsuarioAutenticado, usuario)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// Middleware de autorização por papel
func exigirPapel(papel string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
usuario, ok := r.Context().Value(chaveUsuarioAutenticado).(UsuarioAutenticado)
if !ok || usuario.Papel != papel {
http.Error(w, "acesso negado", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// Helper para extrair usuário do context
func usuarioDoContext(r *http.Request) (UsuarioAutenticado, bool) {
u, ok := r.Context().Value(chaveUsuarioAutenticado).(UsuarioAutenticado)
return u, ok
}
func main() {
r := chi.NewRouter()
// Rotas públicas
r.Group(func(r chi.Router) {
r.Get("/publico", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("acesso livre"))
})
})
// Rotas autenticadas
r.Group(func(r chi.Router) {
r.Use(middlewareJWT)
r.Get("/perfil", func(w http.ResponseWriter, r *http.Request) {
u, _ := usuarioDoContext(r)
w.Write([]byte("olá, " + u.Nome))
})
// Rotas apenas para admin
r.Group(func(r chi.Router) {
r.Use(exigirPapel("admin"))
r.Get("/admin/usuarios", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("lista de usuários — apenas admin"))
})
})
})
http.ListenAndServe(":8080", r)
}
Comparativo: net/http vs Chi vs Gorilla Mux
| Recurso | net/http 1.22 | Chi | Gorilla Mux |
|---|---|---|---|
| Métodos HTTP nas rotas | ✓ | ✓ | ✓ |
| Parâmetros de caminho | ✓ | ✓ | ✓ |
| Regex em parâmetros | ✗ | ✗ | ✓ |
| Agrupamento de rotas | ✗ | ✓ | ✓ |
| Middlewares por grupo | ✗ | ✓ | ✓ |
| Geração de URLs | ✗ | ✗ | ✓ |
| Compatibilidade http.Handler | ✓ | ✓ | ✓ |
| Dependências externas | nenhuma | 1 | 1 |
| Performance relativa | alta | alta | alta |
Resumo do que foi coberto
Este artigo apresentou roteamento avançado com Chi e Gorilla Mux: a filosofia de cada biblioteca, roteamento com parâmetros e agrupamento no Chi, uma API REST completa com injeção de dependências, recursos exclusivos do Gorilla Mux como regex e geração de URLs, middlewares customizados que passam dados via context e um comparativo entre as três opções. O próximo artigo explora o consumo de APIs externas com o cliente HTTP do Go.
Referências e leituras complementares
-
Documentação oficial do Chi — Referência completa e exemplos. https://github.com/go-chi/chi
-
Documentação oficial do Gorilla Mux — Referência e guia de uso. https://github.com/gorilla/mux
-
Chi — Middlewares built-in — Lista de middlewares incluídos no pacote middleware do Chi. https://github.com/go-chi/chi/tree/master/middleware
-
Go 1.22 routing enhancements — Detalhes sobre as melhorias no roteador padrão. https://go.dev/blog/routing-enhancements
-
Benchmark de roteadores Go — Comparativo de performance entre os principais roteadores. https://github.com/julienschmidt/go-http-routing-benchmark
-
httprouter — Roteador minimalista e extremamente performático para comparação. https://github.com/julienschmidt/httprouter
Próximo artigo: Artigo 30 — Consumindo APIs externas com http.Client**
you asked
Sim