Golang

Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi Já leu

12 min de leitura

Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi
Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi Quando a biblioteca padrão não é suficiente O roteador do Go 1.22 resolveu as lacunas mais críticas do —

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


claude response

Comentários

Mais em Golang

Goroutines: concorrência leve e o modelo de execução do Go
Goroutines: concorrência leve e o modelo de execução do Go

Se existe um recurso que mais distingue Go de outras linguagens modernas, é s...

Funções: declaração, múltiplos retornos e variádicas
Funções: declaração, múltiplos retornos e variádicas

Em Go, fun&ccedil;&otilde;es s&atilde;o cidad&atilde;s de primeira classe. El...

Artigo 43 — Profiling e otimização de performance em Go
Artigo 43 — Profiling e otimização de performance em Go

Artigo 43 — Profiling e otimização de performance em Go Otimizar sem medir é...