Roteamento avançado com Gorilla Mux ou Chi

Roteamento avançado com Gorilla Mux ou Chi

Roteamento com Chi e Gorilla Mux: grupos, subroteadores e middlewares por grupo, parâmetros com regex nos dois, geração de URLs, o Timeout do Chi que não interrompe o handler, listas em ordem instável, Schemes atrás de proxy e quando o roteador do Go 1.22 basta.
Golang

• • 18 min de leitura

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. O projeto chegou a ser arquivado no fim de 2022, por falta de mantenedores, e foi retomado em 2023 por um novo grupo — um lembrete de que dependências externas têm ciclo de vida, e de que o roteador da biblioteca padrão não corre esse risco.

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 aceita também parâmetros com expressão regular, como /produtos/{id:[0-9]+}. Para projetos novos que precisam mais do que o roteador padrão oferece, Chi costuma ser a escolha preferida. Em desempenho, os três ficam muito à frente do custo de qualquer acesso a banco ou rede, e isso raramente decide a escolha.

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

Chi e Gorilla Mux resolvem o que o roteador padrão ainda deixa para o programador: grupos de rotas com middlewares próprios, subroteadores e, no caso do Gorilla, restrições por host e geração de URLs. Desde o Go 1.22, porém, a distância diminuiu bastante, e a escolha deixou de ser automática — vale começar pela biblioteca padrão e trazer um roteador externo quando a organização por grupos se repetir o suficiente para justificar a dependência. Em qualquer dos três, o que decide o comportamento em produção continua sendo o mesmo: handlers que respeitam o contexto, ordem de middlewares pensada e respostas com ordem estável.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Uma API com Chi usa r.Use(middleware.Timeout(30 * time.Second)), como no artigo, e mesmo assim tem requisições que ficam presas por minutos quando o banco está lento. O time acha que o middleware "não está funcionando". O que ele faz de fato, e o que falta nos handlers?

Ver resposta

✓ Resposta: O middleware funciona, mas faz menos do que o nome sugere. Ele troca o contexto da requisição por um com prazo e, quando o handler termina, se esse prazo venceu, tenta responder 504. Ele não interrompe o handler, porque nada em Go consegue parar uma goroutine por fora. Num teste com middleware.Timeout(100 * time.Millisecond) e um handler que dorme 1 segundo sem olhar o contexto, a resposta foi 200 com o corpo "terminei", depois de 1 segundo, e o log registrou http: superfluous response.WriteHeader call vindo do próprio middleware, que tentou escrever o 504 quando a resposta já tinha saído. O que falta é os handlers repassarem r.Context() a tudo o que pode demorar: db.QueryContext(ctx, ...), http.NewRequestWithContext(ctx, ...) e select com ctx.Done() nos laços. Aí o prazo cancela a consulta no banco, o handler recebe context deadline exceeded e responde logo. O middleware é só a origem do prazo; quem o respeita é o código que você escreve.

Exercício 2

O endpoint GET /api/v1/produtos da API completa do artigo devolve os mesmos três produtos, mas em ordem diferente a cada chamada. Um cliente que pagina os resultados vê itens repetidos e itens que nunca aparecem. De onde vem a instabilidade, e como corrigir?

Ver resposta

✓ Resposta: O RepoProdutos guarda os dados num map[int]Produto, e o método Listar monta a fatia percorrendo o map com range. Como a ordem de iteração de um map é sorteada a cada laço, a resposta muda de ordem a cada requisição. Para quem só exibe a lista, isso é estranho; para quem pagina, é um defeito: a página 2 é calculada sobre uma ordem diferente da página 1, e itens se repetem ou somem. A correção é ordenar antes de devolver, com slices.SortFunc(lista, func(a, b Produto) int { return cmp.Compare(a.ID, b.ID) }) ou pelo critério que a API documentar. Uma API paginada precisa de uma ordem total e estável, o que em geral significa terminar a ordenação por um campo único, como o ID, mesmo quando o critério principal é outro, como o nome. Com banco de dados, a regra é a mesma: uma consulta sem ORDER BY não tem ordem garantida, ainda que pareça ter nos testes.

Exercício 3

Uma rota do Gorilla Mux registrada com r.Schemes("https") nunca é encontrada em produção, embora o site seja acessado por HTTPS. Localmente, com certificado próprio, ela funciona. O que acontece no caminho entre o navegador e o servidor Go?

Ver resposta

✓ Resposta: Em produção, o HTTPS quase sempre termina antes do servidor Go: num balanceador de carga, num proxy reverso como Nginx ou Caddy, ou num serviço de nuvem. O navegador fala HTTPS com o balanceador, e o balanceador repassa a requisição ao servidor Go por HTTP simples, dentro da rede interna. Para o servidor, a requisição chegou por HTTP, e a restrição Schemes("https") não casa; a rota simplesmente não existe e o cliente recebe 404. Localmente funcionava porque o próprio servidor Go terminava o TLS. O proxy costuma informar o esquema original no cabeçalho X-Forwarded-Proto, mas só se deve confiar nele quando a requisição vem, garantidamente, do proxy, porque qualquer cliente pode enviar esse cabeçalho. A solução mais simples é tirar essa decisão do roteador: o proxy força o redirecionamento para HTTPS, e o servidor Go, que só é alcançável pelo proxy, não precisa distinguir esquemas. O mesmo raciocínio vale para o IP do cliente: o middleware.RealIP do Chi lê cabeçalhos de proxy, e deve ser usado só atrás de um proxy confiável.

Exercício 4

A tabela comparativa deste artigo dizia, numa versão anterior, que o Chi não aceita expressões regulares nos parâmetros. Um desenvolvedor escolheu o Gorilla Mux só por esse motivo. O Chi aceita? E, com o Go 1.22 disponível, quando vale a pena trazer um roteador externo para o projeto?

Ver resposta

✓ Resposta: Aceita. No Chi 5.3.2, a rota /produtos/{id:[0-9]+} respondeu 200 para /produtos/42 e 404 para /produtos/abc, exatamente como a rota equivalente do Gorilla Mux, e a tabela foi corrigida. Com o Go 1.22, a pergunta passou a ser o que o roteador padrão não faz. Ele já cobre método na rota, parâmetros de caminho, precedência por especificidade, 405 automático e detecção de conflitos na inicialização; e, com o Chi 5.3.2, até r.PathValue funciona nas rotas do Chi, o que reduz a diferença entre os dois estilos de código. O que o Chi acrescenta de fato são grupos de rotas com middlewares próprios, subroteadores montados com Mount e um conjunto pronto de middlewares. Se o projeto é pequeno, ou os grupos se resolvem compondo ServeMux e embrulhando handlers, o roteador padrão basta e dispensa uma dependência. Quando a organização por grupos, como uma área pública, uma autenticada e uma de administração, se repete em muitos lugares, o Chi se paga. A validação por regex, por si só, raramente justifica a troca, porque também pode ser feita no handler.

Exercício 5

No exemplo de autorização com Chi, as rotas de administração ficam em r.Group aninhado, com middlewareJWT no grupo de fora e exigirPapel("admin") no de dentro. Um colega move o exigirPapel para antes do middlewareJWT, no mesmo grupo. O que acontece com um administrador legítimo, e por que a ordem dos middlewares é parte da lógica?

Ver resposta

✓ Resposta: O administrador passa a receber 403, "acesso negado". Os middlewares rodam na ordem em que são registrados, do de fora para o de dentro: o primeiro a receber a requisição é o primeiro Use. Com exigirPapel antes, ele procura o usuário no contexto antes de o middlewareJWT ter validado o token e gravado o usuário; r.Context().Value(...) devolve nil, a asserção de tipo falha, e a requisição é negada. A ordem não é detalhe de organização: cada middleware pode depender do que os anteriores gravaram no contexto, e a cadeia é, na prática, um pequeno programa sequencial. Um erro na direção oposta seria pior: se a verificação de papel fosse removida por engano, ou colocada num grupo que não tem autenticação, as rotas ficariam abertas. Por isso vale ter testes com httptest para os três casos de cada rota protegida, sem token, com token de usuário comum e com token de administrador, que pegam tanto o 403 indevido quanto o acesso indevido.

Comentários

Mais em Golang

A biblioteca padrão: um tour pelas principais ferramentas
A biblioteca padrão: um tour pelas principais ferramentas

Tour pela biblioteca padrão do Go: fmt, strings, strconv, time, os, io e…

sync.WaitGroup e sync.Mutex: coordenando goroutines
sync.WaitGroup e sync.Mutex: coordenando goroutines

WaitGroup e Mutex em Go: Add antes de go e o wg.Go do Go 1.25, Mutex e RWMutex…

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

Funções em Go: parâmetros agrupados, múltiplos retornos e o padrão (resultado…