- A melhoria do ServeMux em
net/http no Go 1.22 adiciona correspondência de padrões mais robusta ao multiplexador HTTP padrão, reduzindo a necessidade de depender de roteadores de terceiros em áreas que antes sofriam com a correspondência simples de caminhos
- O novo mux permite usar métodos HTTP e curingas de caminho como
/task/{id}/ no mesmo padrão, possibilitando conectar o mesmo caminho a handlers diferentes para GET, POST e DELETE
- Os valores correspondentes são lidos com
req.PathValue("id"), e {id}... e {$} permitem controlar todo o caminho subsequente ou exatamente o fim do caminho
- Conflitos de padrões aparecem como panic no momento do registro, informando qual caminho corresponde a ambos os padrões e por que nenhum deles é mais específico que o outro
- Embora mais servidores possam passar a se satisfazer apenas com o mux padrão, roteadores e frameworks de terceiros como
gorilla/mux ou Gin ainda oferecem um conjunto mais amplo de recursos e ferramentas
Mudanças no ServeMux do Go 1.22
- O Go 1.22 deve incluir correspondência de padrões aprimorada no
http.ServeMux, o multiplexador HTTP padrão de net/http
- O
http.ServeMux existente era centrado em correspondência básica de caminhos, por isso bibliotecas de terceiros eram muito usadas para roteamento mais complexo
- O novo mux traz correspondência avançada para dentro da biblioteca padrão, reduzindo a diferença de recursos em relação a pacotes de terceiros
- Como comparação, são usados exemplos de um servidor REST com a abordagem existente da biblioteca padrão e com
gorilla/mux
Forma básica de uso do novo mux
- Para desenvolvedores que já usaram pacotes de roteamento em Go, como
gorilla/mux, a forma de uso do novo mux padrão será familiar
- O exemplo cria um mux com
http.NewServeMux() e registra padrões mais ricos em HandleFunc
mux.HandleFunc("GET /path/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "got path\n")
})
mux.HandleFunc("/task/{id}/", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "handling task with id=%v\n", id)
})
- O primeiro handler inclui o método
GET no padrão, então só funciona para requisições GET que começam com /path/
- O segundo handler corresponde a um único componente de caminho com
{id} em /task/{id}/, e o handler lê o valor com r.PathValue("id")
- Como o Go 1.22 ainda não havia sido lançado no momento do exemplo, recomenda-se executar com
gotip
- Os resultados dos testes mostram diretamente as regras de roteamento
- Uma requisição para
/what/ retorna 404 page not found
- Uma requisição
GET para /path/ retorna got path
- Uma requisição
POST para /path/ retorna Method Not Allowed
- Uma requisição para
/task/f0cd2e/ é tratada com id=f0cd2e
Recursos adicionais de padrões e tratamento de conflitos
- Além dos curingas básicos, o novo
ServeMux permite controlar a correspondência de caminhos com mais precisão
{id}... faz o curinga corresponder a todo o caminho subsequente
{$} corresponde estritamente ao fim do caminho
- Tratamento de regras de prioridade e conflitos entre padrões
- A possibilidade de conflitos foi uma área que recebeu atenção especial na proposta
- Por exemplo, os dois padrões a seguir correspondem a uma requisição para
/task/0/status/
mux.HandleFunc("/task/{id}/status/", ...)
mux.HandleFunc("/task/0/{action}/", ...)
- Nesse caso, o novo
ServeMux gera um panic no momento do registro
- A mensagem de erro mostra um caminho de exemplo ao qual os dois padrões correspondem, além do motivo pelo qual nenhum padrão é mais específico que o outro
- Em código complexo que registra padrões em vários lugares, esse diagnóstico detalhado de conflitos é especialmente útil
Reimplementação do exemplo de servidor de tasks
- O servidor de tasks/todo-list da série existente REST Servers in Go foi reimplementado com o mux aprimorado do Go 1.22
- O código completo está no exemplo stdlib-newmux
- Os padrões registrados usam métodos HTTP junto com curingas de caminho
mux.HandleFunc("POST /task/", server.createTaskHandler)
mux.HandleFunc("GET /task/", server.getAllTasksHandler)
mux.HandleFunc("DELETE /task/", server.deleteAllTasksHandler)
mux.HandleFunc("GET /task/{id}/", server.getTaskHandler)
mux.HandleFunc("DELETE /task/{id}/", server.deleteTaskHandler)
mux.HandleFunc("GET /tag/{tag}/", server.tagHandler)
mux.HandleFunc("GET /due/{year}/{month}/{day}/", server.dueHandler)
- Como no exemplo com
gorilla/mux, o mesmo caminho pode ser dividido em handlers por método HTTP
- No
http.ServeMux anterior, vários métodos do mesmo caminho caíam no mesmo handler, e era preciso verificar o método dentro do handler
- No novo mux, mais decisões de roteamento são tratadas fora do handler, facilitando a separação dos handlers
Uso de PathValue e limitações
getTaskHandler lê o ID extraído do caminho com req.PathValue("id") e o converte para inteiro com strconv.Atoi
- Como o padrão
{id} do novo mux não especifica uma expressão regular que corresponda apenas a inteiros, é necessário tratar erros de strconv.Atoi
- Se a conversão do ID falhar, a resposta é
http.StatusBadRequest; se a task não for encontrada no armazenamento, a resposta é http.StatusNotFound
- A implementação final é muito parecida com a solução da Parte 2 que usa
gorilla/mux
- Se necessário, o curinga
{$} do novo http.ServeMux pode limitar facilmente o caminho subsequente
O lugar do mux padrão e dos roteadores de terceiros
- Para iniciantes em Go, “qual pacote de roteamento devo usar?” era uma pergunta frequente
- Depois do Go 1.22, muitos usuários podem sentir que o novo mux padrão já é suficiente sem pacotes de terceiros
- Roteadores como
gorilla/mux ainda oferecem mais recursos que a biblioteca padrão
- Frameworks leves como Gin oferecem não só um roteador, mas também ferramentas adicionais para criar backends web
- O fortalecimento dos recursos da biblioteca padrão é positivo tanto para usuários de pacotes de terceiros quanto para quem usa apenas a biblioteca padrão
1 comentários
Comentários do Hacker News
Gerar um panic quando duas rotas dão match ao mesmo tempo parece contraintuitivo. Outros frameworks web normalmente usam a primeira rota correspondente registrada, então fico curioso se há algum motivo específico do Go para isso
O caso-limite de que “é possível registrar rotas HTTP em vários lugares, então é difícil encontrar matches duplicados” parece algo que poderia ser resolvido com ferramentas. Ao longo da minha carreira, usei bastante o comportamento de “o primeiro match vence”, e também houve muitas exigências de negócio em que
/foo/barprecisava ser uma rota separada, enquanto/foo/{id}ficava como outra rotaA proposta de design[1] diz que, ao fazer match com o padrão mais específico, é possível manter a independência de ordem dos padrões do ServeMux original, mas pode não ser fácil perceber de relance qual dos dois padrões é mais específico ou por que há conflito. Por isso, a mensagem de panic exibida ao registrar padrões conflitantes mostra o conflito usando um caminho de exemplo
A discussão de contexto[2] também explica que a semântica do mux não depende da ordem das chamadas a
HandleouHandleFunc. Graças à independência de ordem, a ordem de inicialização dos pacotes não importa e a refatoração do código fica mais fácil. Por isso, não usar a ordem de registro como critério de desempate, gerar panic em registros duplicados e evitar semânticas que dependam da ordem de registro continuam sendo objetivos centrais do design[1]: https://github.com/golang/go/issues/61410
[2]: https://github.com/golang/go/discussions/60227
Recentemente, em um projeto pessoal, usando gorilla/mux, encontrei um bug em que criei rotas sobrepostas por engano; se o roteador tivesse gerado um panic em vez de executar, ele teria me levado a refatorar as URLs e eu teria evitado o bug por completo
initpor arquivo em GoSe você precisa de um comportamento como
/foo/bare/foo/{id}, basta criar um handler que receba o caminho base e, dentro dele, desvie explicitamente para outras funções de handler. Se quiser um comportamento que não existe na biblioteca padrão, também há muitas outras bibliotecas com mais recursosA biblioteca padrão e a linguagem Go sempre tenderam ao lado conservador diante de ambiguidades possíveis, e isso torna mais fácil construir uma intuição forte e precisa sobre como a biblioteca padrão se comporta. Ser capaz de ler e entender o código de forma rápida e correta vale muito mais do que economizar alguns segundos ao escrevê-lo
/foo/bare/foo/{id}seriam permitidas. Como a primeira é mais específica e teria prioridade, parece okMas, se houver
/foo/{id}/deletee/foo/bar/{action}, então/foo/bar/deletedá match com ambas, e nenhuma das duas é mais específica, então parece que haveria panic. Parece razoável, embora uma prioridade em que a primeira registrada vence talvez fosse melhor$no finalO projeto gorilla/mux é meio estranho e confuso. No ano passado, os mantenedores arquivaram o projeto, então migrei para outro multiplexador, o gin-gonic
Mas, como ele foi mencionado no texto, fui conferir de novo e vi que já não estava mais arquivado. Não sei se isso coloca em dúvida a estabilidade do projeto como um todo, ou se é prova de que open source é realmente estável, no sentido de que, mesmo quando desenvolvedores saem, outras pessoas podem assumir e continuar a manutenção. Ainda assim, é bom que esse recurso seja oferecido pelo próprio Go
[0] https://gorilla.github.io/blog/2023-07-17-project-status-upd...
[1] https://www.reddit.com/r/golang/comments/1528e25/gorilla_web...
[2] https://news.ycombinator.com/item?id=36935541
Como já escrevi antes em um comentário[1], acho que a sintaxe está errada na proposta
Para definir um handler, é preciso criar uma string mágica esquisita. Não entendo por que não transformar isso em argumentos reais; assim, também seria mais fácil usar as constantes existentes
[1] https://github.com/golang/go/issues/61410#issuecomment-16580...
Se adicionassem um novo argumento à interface Mux, o código existente quebraria; como não dá para mudar a assinatura do método, essa string mágica parece um compromisso razoável. Os métodos HTTP não vão mudar tão cedo, e também é fácil validar no momento do registro ou por análise estática. Não vejo muito bem que valor usar constantes traria, especialmente se isso quebrasse a compatibilidade retroativa ou obrigasse a manter para sempre, em paralelo, uma nova API de mux apenas ligeiramente diferente da API existente
{parameter}, isso já não é uma string mágica?Dá para ver, nos termos da RFC2616, como um
Request-URIcom um pouco de mágica de parâmetros, e pensar que agora ele vira algo parecido com uma linha de requisição:[ Method SP ] Request-URI. É totalmente compatível com versões anteriores e, mesmo sem olhar a documentação, fica quase claro o que fazMethodé uma de algumas constantes predefinidas ouextension-method = token, etokenexclui espaços, barras e vários tipos de parênteses; então, mesmo com um método customizado bem estranho, não parece haver risco de confusão ou parsing incorreto.Get()e.Post()nunca resolveram nada para mim. Um único método de registro colocando o método dentro da string provavelmente também é totalmente aceitávelHEADNão gosto dessa abordagem. Fico curioso se há algum motivo para usar um prefixo de método convertido em string
A segurança de tipos de métodos por verbo, como
mux.Getemux.Post, é melhor do que strings mágicas validadas em runtime. Também permite autocomplete do editor e IntelliSensefunc Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }Omiti os tipos só para parecer simples
Interessante. Agora que o mux consegue fazer match também pelo método, fiquei curioso sobre o que acontece quando a rota bate, mas o método não. A questão é se seria 404 ou 405
Conferi e ele retorna 405, com o cabeçalho
Allowpreenchido corretamentehttps://cs.opensource.google/go/go/+/master:src/net/http/ser...
Sei que muita gente não gosta de interfaces baseadas em string, mas acho mais provável alguém digitar corretamente a coisa errada do que errar o nome de um método HTTP por typo. Pessoalmente, acho suficiente que a análise estática avise sobre sintaxe inválida
Ainda assim, se você já tem requisitos avançados, acho melhor não usar o serve mux padrão. Há muitas opções mais adequadas a diferentes casos de uso e, se você já gera rotas dinamicamente, talvez dê menos trabalho usar seu próprio roteador do que tentar encaixar sua estrutura de dados em um roteador existente
Para caminhos sobrepostos, seria muito melhor fazer match na ordem em que foram definidos, em vez de dar panic
No exemplo, se a rota
/task/0/{action}/tivesse sido definida antes da rota com wildcard, eu esperaria que ela fosse correspondida primeiro. Isso facilitaria definir handlers para casos especiaisEm vez de falhar de uma forma “útil”, acho melhor simplesmente funcionar como eu disse. Tem um cheiro meio estranho e pouco Go
O ServeMux original foi projetado para não respeitar a ordem de registro, porque depender da ordem de registro é perigoso. Um dos objetivos centrais de design do Go é dar suporte a “programação em escala”, e efeitos colaterais estranhos causados por mudanças em código distante são exatamente o tipo de coisa que se quer evitar
Em um exemplo simples, em que todos os registros acontecem em uma única função de um único pacote, a intenção da ordem de registro é clara. Mas, em uma base de código suficientemente grande, os registros provavelmente acontecem em vários lugares e podem vir de arquivos de entrada ou de código gerado. Quem escreve a especificação do código gerado talvez não conheça as sutilezas da ordem de execução e seu impacto no roteamento. Só uma mudança na ordenação lexicográfica dos imports de pacotes poderia alterar o roteamento de forma inesperada
Evitar resultados inesperados, facilitar refatorações e tornar programas complexos mais fáceis de raciocinar é claramente uma direção Go. Não é preciso concordar sempre com a escolha, mas a justificativa é consistente
Gosto de reduzir uma dependência externa. Como observação lateral, a combinação sqlc + Postgres + templ + htmx + Tailwind foi uma stack de altíssima produtividade de desenvolvimento. templ parece um JSX para Go
Também preciso dar uma olhada no htmx
Eu esperava que o bud com compilação de Svelte cobrisse essa lacuna, mas o templ parece uma alternativa melhor
Realmente não gosto desta proposta. Colocar o método de requisição HTTP dentro da URI — e ainda só às vezes — e talvez até fazer algo como
POST,PUT,PATCH /something? Prefiro passarSe for mesmo necessário, bastaria criar um método dedicado que receba o nome do método de requisição HTTP
Fico curioso para saber que problema um método dedicado que recebe o nome do método HTTP resolveria em comparação a incluir o método na string
Poderia haver um argumento separado para a lista de métodos ou opções
Go não é minha ferramenta principal, mas já usei Gin, e é assim:
router.GET("/", func(context *gin.Context) { ... })Não parece muito diferente de várias outras linguagens e frameworks. Fico curioso se há mais exemplos que usem o formato
GET /path/Uma equipe oficial da linguagem usar strings prefixadas em vez de enums parece uma escolha de design muito estranha
Por exemplo,
(Http.GET, "/path")parece melhor do que o atual("GET /path"). EstranhoMesmo assim, eu preferiria ter várias funções como
mux.GET,mux.POSTa essa abordagem