1 pontos por GN⁺ 2023-10-17 | 1 comentários | Compartilhar no WhatsApp
  • 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

 
GN⁺ 2023-10-17
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/bar precisava ser uma rota separada, enquanto /foo/{id} ficava como outra rota

    • Como não dá para garantir que a ordem de registro seja sempre a mesma, isso vira, na prática, um comportamento indefinido. O ServeMux original foi projetado e implementado assim, e a ideia foi considerar útil continuar dando suporte a esse comportamento
      A 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 Handle ou HandleFunc. 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
    • O motivo para usar uma linguagem compilada e tipada como Go é trazer os bugs mais para a esquerda. Um bug detectado na inicialização da aplicação é melhor do que um bug que estoura como caso de canto em produção, e um bug detectado na compilação é melhor do que um bug detectado na inicialização
      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
    • O panic em si é uma ferramenta. Ele impede o uso de código ambíguo e difícil de raciocinar, e evita depender de fatores que podem mudar arbitrariamente, como a ordem de inicialização. Por exemplo, apenas mudar o nome de um arquivo já pode afetar a ordem das funções init por arquivo em Go
      Se você precisa de um comportamento como /foo/bar e /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 recursos
      A 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
    • Acho que /foo/bar e /foo/{id} seriam permitidas. Como a primeira é mais específica e teria prioridade, parece ok
      Mas, se houver /foo/{id}/delete e /foo/bar/{action}, então /foo/bar/delete dá 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
    • Receber um erro é muito melhor para depurar do que receber dados errados porque a rota que de fato foi executada era outra. Já passei por isso de verdade no Django por causa de uma rota em que faltava o regex $ no final
  • O 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

  • 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...

    • Acho que isso é por causa da garantia de compatibilidade retroativa do Go
      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
    • Por causa do parsing de {parameter}, isso já não é uma string mágica?
      Dá para ver, nos termos da RFC2616, como um Request-URI com 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 faz
      Method é uma de algumas constantes predefinidas ou extension-method = token, e token exclui 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
    • Concordo que é meio estranho, mas o motivo é claro: eles não querem quebrar nem alterar interfaces públicas existentes
    • No começo tive a mesma impressão, mas, sendo sincero, ao usar Gin, métodos como .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ável
    • Não entendo qual é o problema de registrar automaticamente a rota HEAD
  • Nã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.Get e mux.Post, é melhor do que strings mágicas validadas em runtime. Também permite autocomplete do editor e IntelliSense

    • Isso pode ser adicionado com muita facilidade. Se isso incomoda tanto, basta criar algo como func Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }
      Omiti os tipos só para parecer simples
    • Eu também não gosto. Quero ter certeza de que o roteamento funciona em tempo de compilação, não em runtime
    • Se você quer segurança de tipos, escolha uma linguagem com segurança de tipos
    • Não prefiro o uso de strings, mas, sendo justo, quando a requisição chega, o método HTTP no fim das contas também é uma string. Há uma certa elegância em ele corresponder ao prefixo da primeira linha do pacote HTTP
    • Esse assunto é tratado amplamente na proposta
  • 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 Allow preenchido corretamente
    https://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 especiais
    Em vez de falhar de uma forma “útil”, acho melhor simplesmente funcionar como eu disse. Tem um cheiro meio estranho e pouco Go

    • A semântica da linguagem importa, e esta é de fato uma escolha bem ao estilo 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

    • Até agora tenho tido uma experiência bem boa usando sqlc
      Também preciso dar uma olhada no htmx
    • Fico curioso sobre como o Tailwind é tratado aqui. A ideia de adicionar Tailwind é muito boa, mas, se eu tiver que usar npm, prefiro escrever CSS diretamente
    • No começo achei que fosse um typo de template, mas na verdade era um pacote chamado templ, diferente da biblioteca padrão. Dei uma olhada rápida na documentação e parece bem limpo
      Eu esperava que o bud com compilação de Svelte cobrisse essa lacuna, mas o templ parece uma alternativa melhor
    • Eu adoraria que o templ tivesse suporte no IntelliJ. Gosto da ideia
  • 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 passar
    Se for mesmo necessário, bastaria criar um método dedicado que receba o nome do método de requisição HTTP

    • Não entendo por quê. Na prática, não muda muita coisa. Acho que não dá para odiar uma proposta inteira só por uma questão simples de estilo
      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
    • Concordo. Projetar assim não faz muito sentido
      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/

    • Fora compatibilidade retroativa, provavelmente quase não há motivo para fazer assim
    • Na prática, nunca vi. É simplesmente uma abordagem ruim
  • 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"). Estranho

    • É uma forma de cumprir a promessa de compatibilidade do Go 1
    • Como desenvolvedor Go, nesses casos eu gostaria que houvesse sobrecarga de funções para manter compatibilidade retroativa. A promessa de compatibilidade é ótima, mas as esquisitices que ela gera não são legais
    • É um erro que não dá para corrigir sem quebrar código existente ou introduzir uma nova função
      Mesmo assim, eu preferiria ter várias funções como mux.GET, mux.POST a essa abordagem