- 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
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/bardevait être une route distincte et/foo/{id}une autre routeLa 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 à
HandleouHandleFunc. 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
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
initau niveau des fichiers en GoSi l’on a besoin d’un comportement comme
/foo/baret/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ésLa 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/baret/foo/{id}devraient probablement être autorisées. La première étant plus spécifique, elle a la priorité, donc cela semble correctEn revanche, avec
/foo/{id}/deleteet/foo/bar/{action},/foo/bar/deletecorrespond 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$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
[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
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...
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
{parameter}, ce n’est pas déjà une chaîne magique ?En termes RFC2616, on peut voir ça comme une
Request-URIavec 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 faitMethodest soit l’une de quelques constantes prédéfinies, soitextension-method = token, et commetokenexclut 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.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 correcteHEADJe 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.Getoumux.Postest meilleure que des chaînes magiques validées à l’exécution. Cela permet aussi l’autocomplétion de l’éditeur ou IntelliSensefunc Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }J’ai omis les types pour garder l’exemple simple
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
Allowcorrectement remplihttps://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 particuliersJe 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
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
Il faut aussi que je jette un œil à htmx
J’espérais que bud, avec la compilation Svelte, comblerait ce manque, mais templ semble être une meilleure alternative
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 merciS’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 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
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/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"). BizarreMalgré tout, je préférerais plusieurs fonctions comme
mux.GET,mux.POSTplutôt que cette approche