1 points par GN⁺ 2023-10-17 | 1 commentaires | Partager sur WhatsApp
  • L’amélioration de ServeMux dans net/http avec Go 1.22 apporte une correspondance de motifs plus puissante au multiplexeur HTTP standard, réduisant les cas où l’on dépendait de routeurs tiers pour de simples correspondances de chemins
  • Le nouveau mux permet d’utiliser dans les motifs à la fois des méthodes HTTP et des jokers de chemin comme /task/{id}/, ce qui permet d’associer un même chemin à des handlers différents selon GET, POST ou DELETE
  • Les valeurs correspondantes se lisent avec req.PathValue("id"), et {id}... ainsi que {$} permettent de contrôler respectivement tout le chemin qui suit ou la fin exacte du chemin
  • Les conflits de motifs se manifestent par un panic au moment de l’enregistrement, avec une indication du chemin qui correspond aux deux côtés et de la raison pour laquelle aucun n’est plus spécifique que l’autre
  • Davantage de serveurs pourront se contenter du mux standard, mais les routeurs et frameworks tiers comme gorilla/mux ou Gin continuent d’offrir un éventail plus large de fonctionnalités et d’outils

Les changements de ServeMux dans Go 1.22

  • Go 1.22 doit intégrer une correspondance de motifs renforcée dans http.ServeMux, le multiplexeur HTTP de base de net/http
  • L’ancien http.ServeMux était surtout centré sur une correspondance de chemins élémentaire, si bien que des bibliothèques tierces étaient souvent utilisées pour un routage plus complexe
  • Le nouveau mux apporte une correspondance avancée dans la bibliothèque standard et réduit l’écart fonctionnel avec les paquets tiers
  • La comparaison s’appuie sur la méthode existante de la bibliothèque standard et sur un exemple de serveur REST utilisant gorilla/mux

Mode d’utilisation de base du nouveau mux

  • Les développeurs ayant déjà utilisé un paquet de routage Go comme gorilla/mux trouveront l’utilisation du nouveau mux standard familière
  • L’exemple crée un mux avec http.NewServeMux() et enregistre des motifs plus riches avec 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)
})
  • Le premier handler inclut la méthode GET dans le motif, il ne s’applique donc qu’aux requêtes GET commençant par /path/
  • Le second handler fait correspondre un seul composant de chemin avec {id} dans /task/{id}/, puis lit la valeur dans le handler avec r.PathValue("id")
  • Comme il s’agit d’un exemple datant d’avant la sortie de Go 1.22, l’exécution avec gotip est recommandée
  • Les résultats des tests permettent de vérifier immédiatement les règles de routage
    • Une requête sur /what/ donne 404 page not found
    • Une requête GET sur /path/ donne got path
    • Une requête POST sur /path/ donne Method Not Allowed
    • Une requête sur /task/f0cd2e/ est traitée avec id=f0cd2e

Fonctions de motifs supplémentaires et gestion des conflits

  • Le nouveau ServeMux permet, au-delà des jokers de base, un contrôle plus fin de la correspondance de chemins
    • {id}... pour faire correspondre au joker tout le chemin qui suit
    • {$} pour faire correspondre strictement la fin du chemin
    • Gestion des priorités et des règles de conflit entre motifs
  • La possibilité de conflits a fait l’objet d’une attention particulière dans la proposition
  • Par exemple, les deux motifs suivants correspondent tous deux à une requête /task/0/status/
mux.HandleFunc("/task/{id}/status/", ...)
mux.HandleFunc("/task/0/{action}/", ...)
  • Dans ce cas, le nouveau ServeMux déclenche un panic au moment de l’enregistrement
  • Le message d’erreur montre un exemple de chemin auquel les deux motifs correspondent, ainsi que la raison pour laquelle aucun motif n’est plus spécifique que l’autre
  • Dans du code complexe qui enregistre des motifs à plusieurs endroits, ce diagnostic détaillé des conflits est particulièrement utile

Réimplémentation de l’exemple de serveur task

  • Le serveur task/todo-list de l’ancienne série REST Servers in Go a été réimplémenté avec le mux amélioré de Go 1.22
  • Le code complet se trouve dans l’exemple stdlib-newmux
  • Les motifs enregistrés utilisent à la fois les méthodes HTTP et les jokers de chemin
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)
  • Comme dans l’exemple avec gorilla/mux, un même chemin peut être réparti entre des handlers par méthode HTTP
  • Avec l’ancien http.ServeMux, plusieurs méthodes sur un même chemin aboutissaient au même handler, qui devait vérifier la méthode en interne
  • Avec le nouveau mux, une plus grande part de la décision de routage est traitée en dehors du handler, ce qui facilite leur séparation

Utilisation de PathValue et limites

  • getTaskHandler lit l’ID extrait du chemin avec req.PathValue("id") et le convertit en entier avec strconv.Atoi
  • Comme le motif {id} du nouveau mux ne permet pas de spécifier une expression régulière ne correspondant qu’aux entiers, il faut gérer les erreurs de strconv.Atoi
  • Si la conversion de l’ID échoue, il répond avec http.StatusBadRequest; si la task est introuvable dans le stockage, il répond avec http.StatusNotFound
  • L’implémentation finale ressemble beaucoup à la solution de la Partie 2 utilisant gorilla/mux
  • Si nécessaire, le joker {$} du nouveau http.ServeMux permet de restreindre facilement le chemin qui suit

Place du mux standard et des routeurs tiers

  • Pour les débutants en Go, « quel paquet de routage utiliser » était une question fréquente
  • Après Go 1.22, de nombreux utilisateurs pourront estimer que le nouveau mux standard leur suffit, sans paquet tiers
  • Les routeurs comme gorilla/mux offrent toujours davantage de fonctionnalités que la bibliothèque standard
  • Les frameworks légers comme Gin fournissent non seulement un routeur, mais aussi des outils supplémentaires pour construire des backends web
  • Le renforcement des fonctionnalités de la bibliothèque standard est une évolution positive aussi bien pour les utilisateurs de paquets tiers que pour ceux qui s’en tiennent à la bibliothèque standard

1 commentaires

 
GN⁺ 2023-10-17
Avis sur Hacker News
  • Déclencher un panic quand deux routes correspondent en même temps semble contre-intuitif. Les autres frameworks web utilisent généralement la première route correspondante enregistrée ; je me demande s’il y a une raison propre à Go
    Le cas limite selon lequel « des routes HTTP peuvent être enregistrées à plusieurs endroits, ce qui rend les correspondances en double difficiles à trouver » semble pouvoir être résolu par des outils. Tout au long de ma carrière, j’ai beaucoup exploité le comportement « la première correspondance gagne », et il y a souvent eu des besoins métier où /foo/bar devait être une route distincte et /foo/{id} une autre route

    • Comme on ne peut pas garantir que l’ordre d’enregistrement sera toujours le même, cela devient en pratique un comportement indéfini. C’est ainsi que ServeMux a été conçu et implémenté à l’origine, et il a été jugé utile de continuer à prendre en charge ce comportement
      La proposition de conception[1] indique que faire correspondre le motif le plus spécifique permet de préserver l’indépendance vis-à-vis de l’ordre des motifs du ServeMux d’origine, mais qu’il peut être difficile de voir d’un coup d’œil lequel des deux motifs est le plus spécifique, ou pourquoi ils entrent en conflit. Le message de panic émis lors de l’enregistrement de motifs en conflit montre donc le conflit au moyen d’exemples de chemins
      La discussion de contexte[2] explique également que la sémantique du mux ne dépend pas de l’ordre des appels à Handle ou HandleFunc. Grâce à l’indépendance de l’ordre, l’ordre d’initialisation des packages n’a pas d’importance et le refactoring du code est plus facile. C’est pourquoi le critère de départage n’est pas l’ordre d’enregistrement, les doublons d’enregistrement provoquent un panic, et éviter une sémantique dépendante de l’ordre d’enregistrement reste un objectif central de conception
      [1]: https://github.com/golang/go/issues/61410
      [2]: https://github.com/golang/go/discussions/60227
    • La raison d’utiliser un langage compilé et typé comme Go est de faire remonter les bugs plus tôt dans le cycle. Un bug détecté au démarrage de l’application vaut mieux qu’un bug qui explose dans un cas limite en production, et un bug détecté à la compilation vaut encore mieux qu’un bug détecté au démarrage
      Sur un projet personnel récent, en utilisant gorilla/mux, je suis tombé sur un bug où j’avais créé par erreur des routes qui se chevauchaient ; si le routeur avait déclenché un panic au lieu de s’exécuter, cela m’aurait poussé à refactorer les URL et aurait permis d’éviter complètement le bug
    • Le panic lui-même est un outil. Il empêche d’utiliser du code ambigu et difficile à raisonner, et évite de dépendre d’éléments susceptibles de changer arbitrairement, comme l’ordre d’initialisation. Par exemple, un simple changement de nom de fichier peut influencer l’ordre des fonctions init au niveau des fichiers en Go
      Si l’on a besoin d’un comportement comme /foo/bar et /foo/{id}, il suffit de créer un handler qui reçoit le chemin de base et de faire, à l’intérieur, une répartition explicite vers d’autres fonctions handler. Si l’on veut un comportement qui n’existe pas dans la bibliothèque standard, il existe aussi de nombreuses autres bibliothèques plus riches en fonctionnalités
      La bibliothèque standard et le langage Go ont toujours eu tendance à choisir l’approche conservatrice face aux ambiguïtés possibles, ce qui facilite la construction d’une intuition forte et précise de la façon dont la bibliothèque standard se comporte. Pouvoir lire et comprendre le code rapidement et correctement vaut bien plus que gagner quelques secondes en l’écrivant
    • /foo/bar et /foo/{id} devraient probablement être autorisées. La première étant plus spécifique, elle a la priorité, donc cela semble correct
      En revanche, avec /foo/{id}/delete et /foo/bar/{action}, /foo/bar/delete correspond aux deux, et aucune n’est plus spécifique que l’autre, donc cela devrait déclencher un panic. Cela paraît raisonnable, même si une priorité où la première route enregistrée gagne pourrait être préférable
    • Recevoir une erreur est bien meilleur pour le débogage que recevoir de mauvaises données parce que la route effectivement exécutée était une autre route. J’ai déjà vécu cela avec Django, à cause d’une route à laquelle il manquait l’expression régulière finale $
  • Le projet gorilla/mux est un peu étrange et déroutant. L’an dernier, les mainteneurs ont archivé le projet, si bien que je suis passé à un autre multiplexeur, gin-gonic
    Mais comme il est mentionné dans l’article, je suis retourné vérifier, et il n’est plus archivé. Je ne sais pas si cela remet en question la stabilité globale du projet, ou si c’est au contraire une preuve que l’open source est réellement stable, dans la mesure où d’autres peuvent prendre le relais de la maintenance quand des développeurs s’en vont. Quoi qu’il en soit, c’est une bonne chose que cette fonctionnalité soit fournie directement par Go

  • Je l’avais déjà écrit dans un commentaire[1], mais je trouve que la syntaxe de la proposition est mauvaise
    Pour définir un handler, il faut fabriquer une étrange chaîne magique. Je ne comprends pas pourquoi ne pas en faire de vrais arguments ; cela permettrait aussi d’utiliser plus facilement les constantes existantes
    [1] https://github.com/golang/go/issues/61410#issuecomment-16580...

    • Je pense que c’est à cause de la garantie de rétrocompatibilité de Go
      Ajouter un nouvel argument à l’interface Mux casserait le code existant, et comme on ne peut pas changer la signature des méthodes, ce genre de chaîne magique ressemble à un compromis raisonnable. Les méthodes HTTP ne vont pas changer de sitôt, et il est facile de les valider au moment de l’enregistrement ou par analyse statique. Je ne vois pas très bien quelle valeur apporteraient des constantes, surtout si cela casse la rétrocompatibilité ou oblige à maintenir indéfiniment une nouvelle API de mux à peine différente de l’API existante
    • Avec le parsing de {parameter}, ce n’est pas déjà une chaîne magique ?
      En termes RFC2616, on peut voir ça comme une Request-URI avec un peu de magie de paramètres, à laquelle on ajoute quelque chose qui ressemble à une ligne de requête, [ Method SP ] Request-URI. C’est totalement rétrocompatible, et même sans lire la doc, on comprend presque clairement ce que ça fait
      Method est soit l’une de quelques constantes prédéfinies, soit extension-method = token, et comme token exclut les espaces, les slashs et divers types de parenthèses, même une méthode custom très bizarre ne semble pas susceptible d’être confondue ou mal parsée
    • Je suis d’accord que c’est un peu étrange, mais la raison est claire : ils ne veulent pas casser ni modifier l’interface publique existante
    • J’ai eu la même impression au début, mais honnêtement, quand j’utilise Gin, les méthodes comme .Get() ou .Post() ne m’ont jamais vraiment résolu quoi que ce soit. Une méthode d’enregistrement unique qui met la méthode dans la chaîne sera probablement tout à fait correcte
    • Je ne vois pas quel est le problème avec l’enregistrement automatique des routes HEAD
  • Je n’aime pas cette approche. Je me demande s’il y a une raison d’utiliser un préfixe de méthode sous forme de chaîne
    La sûreté de typage de méthodes par verbe comme mux.Get ou mux.Post est meilleure que des chaînes magiques validées à l’exécution. Cela permet aussi l’autocomplétion de l’éditeur ou IntelliSense

    • On peut ajouter ça très facilement. Si ça te préoccupe vraiment à ce point, il suffit de faire quelque chose comme func Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }
      J’ai omis les types pour garder l’exemple simple
    • Moi non plus, je n’aime pas trop. J’aimerais avoir la certitude que le routage fonctionne à la compilation, pas à l’exécution
    • Si tu veux de la sûreté de typage, choisis un langage typé sûr
    • Je ne préfère pas l’usage de chaînes, mais pour être juste, quand une requête arrive, la méthode HTTP est elle aussi une chaîne au final. Il y a une certaine élégance à ce que cela corresponde au préfixe de la première ligne du paquet HTTP
    • Ce point est largement traité dans la proposition
  • Intéressant. Maintenant que le mux peut aussi faire correspondre la méthode, je me demandais ce qui se passe quand la route correspond mais pas la méthode : 404 ou 405 ?
    Après vérification, c’est bien un 405 qui est renvoyé, avec l’en-tête Allow correctement rempli
    https://cs.opensource.google/go/go/+/master:src/net/http/ser...
    Je sais que beaucoup de gens n’aiment pas les interfaces basées sur des chaînes, mais je pense qu’il est plus probable de saisir correctement quelque chose de faux que de faire une faute de frappe dans le nom d’une méthode HTTP. Personnellement, une analyse statique qui signale une syntaxe invalide me suffirait
    Cela dit, si l’on a déjà des besoins avancés, mieux vaut probablement ne pas utiliser le serve mux de base. Il existe beaucoup d’options plus adaptées à divers cas d’usage, et si l’on génère déjà des routes dynamiquement, il peut être moins pénible d’écrire son propre routeur que de forcer sa structure de données à rentrer dans un routeur existant

  • Pour les chemins qui se chevauchent, ce serait bien mieux de faire la correspondance dans l’ordre défini plutôt que de paniquer
    Dans l’exemple, si le chemin /task/0/{action}/ avait été défini avant le chemin wildcard, je m’attendrais à ce qu’il corresponde en premier. C’est ce qui permet de définir facilement des handlers pour des cas particuliers
    Je préférerais que ça fonctionne simplement comme je l’ai indiqué, plutôt que d’échouer de manière « utile ». Ça sent un peu bizarre et pas très Go

    • La sémantique du langage est importante, et c’est en fait un choix assez dans l’esprit de Go
      Le ServeMux d’origine a été conçu pour ne pas respecter l’ordre d’enregistrement, car dépendre de cet ordre est dangereux. L’un des objectifs de conception centraux de Go est de prendre en charge la « programmation à grande échelle », et les effets de bord étranges causés par des modifications de code éloignées sont précisément le genre de choses qu’on veut éviter
      Dans un exemple simple où tous les enregistrements se font dans une seule fonction d’un seul package, l’intention de l’ordre d’enregistrement est claire. Mais dans une base de code suffisamment grande, les enregistrements peuvent très bien se produire à plusieurs endroits, voire être le résultat de fichiers d’entrée ou de code généré. La personne qui écrit la spécification du code généré peut ne pas connaître les subtilités de l’ordre d’exécution et leur impact sur le routage. Un simple changement dans le tri lexicographique des imports de packages pourrait modifier le routage de manière inattendue
      Éviter les résultats imprévus, faciliter le refactoring et rendre les programmes complexes plus faciles à raisonner va clairement dans le sens de Go. Il n’est pas nécessaire d’être toujours d’accord avec le choix, mais la raison est cohérente
  • C’est bien d’avoir une dépendance externe en moins. À côté de ça, la combinaison sqlc + Postgres + templ + htmx + Tailwind s’est révélée être une stack à très forte productivité de développement. templ donne l’impression d’un JSX pour Go

    • Jusqu’ici, j’ai une assez bonne expérience avec sqlc
      Il faut aussi que je jette un œil à htmx
    • Je me demande comment Tailwind est géré ici. L’idée d’ajouter Tailwind est vraiment bonne, mais s’il faut utiliser npm, je préfère encore écrire directement du CSS
    • Au début, j’ai cru que c’était une faute de frappe pour template, mais c’était en fait un package différent de la bibliothèque standard, qui s’appelle vraiment templ. En regardant rapidement la documentation, ça a l’air plutôt propre
      J’espérais que bud, avec la compilation Svelte, comblerait ce manque, mais templ semble être une meilleure alternative
    • Ce serait vraiment bien que templ ait un support IntelliJ. J’aime l’idée
  • Cette proposition ne me plaît vraiment pas. Mettre la méthode de requête HTTP dans l’URI, et seulement parfois, voire éventuellement faire des choses comme POST,PUT,PATCH /something, non merci
    S’il faut vraiment le faire, il suffit de créer une méthode dédiée qui prend le nom de la méthode de requête HTTP

    • Je ne vois pas pourquoi. En pratique, ça ne change pas grand-chose. Je ne pense pas qu’on puisse vraiment détester toute la proposition à cause d’une simple question de style
      Je me demande quel problème résoudrait une méthode dédiée prenant le nom de la méthode HTTP par rapport à l’inclusion de la méthode dans la chaîne
    • D’accord. Concevoir ça comme ça n’a pas beaucoup de sens
      Il aurait pu y avoir un argument séparé pour la méthode ou la liste d’options
  • Go n’est pas mon outil principal, mais j’ai utilisé Gin, et ça ressemble à ça :
    router.GET("/", func(context *gin.Context) { ... })
    Ça ne semble pas très différent de beaucoup d’autres langages et frameworks. Je me demande s’il existe d’autres exemples qui utilisent la forme GET /path/

    • À part la rétrocompatibilité, il n’y a pas vraiment de raison de faire comme ça, donc il n’y en a probablement presque pas
    • Je n’en ai jamais vu en pratique. C’est juste une mauvaise approche
  • Voir l’équipe officielle du langage utiliser des chaînes préfixées au lieu d’énumérations semble être un choix de conception très étrange
    Par exemple, (Http.GET, "/path") me paraît mieux que l’actuel ("GET /path"). Bizarre

    • C’est une façon de respecter la promesse de compatibilité de Go 1
    • En tant que développeur Go, dans ce genre de cas, j’aimerais qu’il y ait une surcharge de fonctions pour préserver la rétrocompatibilité. La promesse de compatibilité est excellente, mais les bizarreries qu’elle entraîne le sont beaucoup moins
    • C’est une erreur qu’on ne peut pas corriger sans casser du code existant ou introduire une nouvelle fonction
      Malgré tout, je préférerais plusieurs fonctions comme mux.GET, mux.POST plutôt que cette approche