- La mejora de ServeMux en
net/http de Go 1.22 incorpora un emparejamiento de patrones más potente en el multiplexor HTTP estándar, reduciendo áreas que antes dependían de routers de terceros por las limitaciones del simple emparejamiento de rutas
- El nuevo mux permite usar juntos métodos HTTP y comodines de ruta como
/task/{id}/ dentro de un patrón, por lo que la misma ruta puede conectarse a handlers distintos para GET, POST y DELETE
- Los valores emparejados se leen con
req.PathValue("id"), y con {id}... y {$} se puede controlar toda la ruta que sigue o hasta el final exacto de la ruta
- Los conflictos de patrones aparecen como un panic al momento del registro, indicando qué ruta coincide con ambos y por qué ninguno es más específico que el otro
- Con solo el mux estándar puede bastar para más servidores, aunque routers y frameworks de terceros como
gorilla/mux o Gin siguen ofreciendo un conjunto más amplio de funciones y herramientas
Cambios en ServeMux de Go 1.22
- En Go 1.22 se planea incorporar emparejamiento de patrones mejorado en
http.ServeMux, el multiplexor HTTP básico de net/http
- El
http.ServeMux anterior se centraba en el emparejamiento básico de rutas, por lo que para un enrutamiento más complejo se usaban mucho bibliotecas de terceros
- El nuevo mux lleva capacidades de emparejamiento avanzadas a la biblioteca estándar y reduce la brecha funcional frente a paquetes de terceros
- Como comparación se usan el enfoque previo con la biblioteca estándar y un ejemplo de servidor REST con
gorilla/mux
Uso básico del nuevo mux
- Para quienes ya usaron paquetes de enrutamiento de Go como
gorilla/mux, la forma de usar el nuevo mux estándar resultará familiar
- El ejemplo crea un mux con
http.NewServeMux() y registra patrones más expresivos con 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)
})
- El primer handler incluye el método
GET en el patrón, así que solo responde a solicitudes GET que empiecen con /path/
- El segundo handler usa
{id} en /task/{id}/ para emparejar un único componente de ruta, y luego lee el valor con r.PathValue("id")
- Como el ejemplo corresponde a un momento en que Go 1.22 aún no había sido liberado, se recomienda ejecutarlo con
gotip
- Los resultados de prueba permiten comprobar enseguida las reglas de enrutamiento
- La solicitud a
/what/ devuelve 404 page not found
- Una solicitud
GET a /path/ devuelve got path
- Una solicitud
POST a /path/ devuelve Method Not Allowed
- Una solicitud a
/task/f0cd2e/ se procesa con id=f0cd2e
Funciones adicionales de patrones y manejo de conflictos
- El nuevo
ServeMux permite un control más fino del emparejamiento de rutas además de los comodines básicos
{id}... empareja toda la ruta restante con el comodín
{$} empareja de forma estricta el final de la ruta
- También gestiona reglas de prioridad y conflicto entre patrones
- La posibilidad de conflictos fue un punto especialmente cuidado en la propuesta
- Por ejemplo, los siguientes dos patrones coinciden ambos con una solicitud a
/task/0/status/
mux.HandleFunc("/task/{id}/status/", ...)
mux.HandleFunc("/task/0/{action}/", ...)
- En este caso, el nuevo
ServeMux genera un panic en el momento del registro
- El mensaje de error muestra una ruta de ejemplo que coincide con ambos patrones y explica por qué ninguno es más específico que el otro
- En código complejo donde los patrones se registran en varios lugares, este diagnóstico detallado de conflictos resulta especialmente útil
Reimplementación del ejemplo de servidor task
- Se vuelve a implementar el servidor de task/todo-list de la serie REST Servers in Go usando el mux mejorado de Go 1.22
- El código completo está en el ejemplo stdlib-newmux
- Los patrones registrados combinan métodos HTTP y comodines de ruta
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)
- Igual que en el ejemplo con
gorilla/mux, la misma ruta puede dividirse en handlers por método HTTP
- En el
http.ServeMux anterior, varios métodos para una misma ruta entraban al mismo handler y el método tenía que verificarse dentro del propio handler
- En el nuevo mux, más decisiones de enrutamiento se resuelven fuera del handler, lo que facilita separar handlers
Uso de PathValue y limitaciones
getTaskHandler lee el ID extraído de la ruta con req.PathValue("id") y lo convierte a entero con strconv.Atoi
- El patrón
{id} del nuevo mux no permite especificar una expresión regular para emparejar solo enteros, así que hace falta manejar el error de strconv.Atoi
- Si falla la conversión del ID, responde con
http.StatusBadRequest; si no encuentra el task en el almacenamiento, responde con http.StatusNotFound
- La implementación final es muy parecida a la solución de la Parte 2 que usaba
gorilla/mux
- Si hace falta, el comodín
{$} del nuevo http.ServeMux permite restringir fácilmente la ruta restante
El lugar del mux estándar y los routers de terceros
- Para quienes empiezan con Go, una pregunta frecuente era “¿qué paquete de router debería usar?”
- Después de Go 1.22, muchos usuarios podrían sentir que el nuevo mux estándar les basta sin necesidad de paquetes de terceros
- Routers como
gorilla/mux siguen ofreciendo más funciones que la biblioteca estándar
- Frameworks ligeros como Gin no solo ofrecen router, sino también herramientas adicionales para construir backends web
- El fortalecimiento de la biblioteca estándar es un cambio positivo tanto para quienes usan paquetes de terceros como para quienes prefieren usar solo la biblioteca estándar
1 comentarios
Opiniones de Hacker News
Que se lance un panic cuando dos rutas coinciden al mismo tiempo parece poco intuitivo. Otros frameworks web normalmente usan la primera ruta coincidente registrada, y me pregunto si hay alguna razón específica de Go para esto.
El caso límite de que “como se pueden registrar rutas HTTP desde varios lugares, es difícil encontrar coincidencias duplicadas” parece algo que podría resolverse con herramientas. A lo largo de mi carrera he usado mucho el comportamiento de “la primera coincidencia gana”, y también he tenido muchos requisitos de negocio donde
/foo/bardebía ser una ruta separada y/foo/{id}otra ruta distinta.La propuesta de diseño[1] dice que, si se hace coincidir el patrón más específico, se puede mantener la independencia respecto del orden de los patrones del ServeMux original, pero que puede no ser obvio de un vistazo cuál de dos patrones es más específico o por qué entran en conflicto. Por eso, el mensaje de panic al registrar patrones en conflicto muestra el conflicto con una ruta de ejemplo.
En la discusión de contexto[2] también se explica que la semántica del mux no depende del orden de las llamadas a
HandleoHandleFunc. Gracias a la independencia del orden, el orden de inicialización de los paquetes no importa y refactorizar código es más fácil. Por eso, en vez de usar el orden de registro como criterio de desempate, los registros duplicados producen un panic, y evitar semánticas que dependan del orden de registro sigue siendo un objetivo central del diseño.[1]: https://github.com/golang/go/issues/61410
[2]: https://github.com/golang/go/discussions/60227
En un proyecto personal reciente usé gorilla/mux y me topé con un bug en el que por error creé rutas solapadas; si el router hubiera hecho panic en vez de ejecutarse, me habría empujado a refactorizar las URL y habría evitado el bug por completo.
initpor archivo en Go.Si necesitas un comportamiento como
/foo/bary/foo/{id}, puedes crear un handler que reciba la ruta base y, dentro de él, ramificar explícitamente hacia otras funciones handler. Si quieres un comportamiento que no existe en la biblioteca estándar, hay muchas otras bibliotecas con más funciones.La biblioteca estándar y el lenguaje Go siempre han tendido a ser conservadores ante posibles ambigüedades, y gracias a eso es fácil formar una intuición fuerte y precisa sobre cómo se comporta la biblioteca estándar. Poder leer y entender código de forma rápida y correcta es mucho más valioso que ahorrar unos segundos al escribirlo.
/foo/bary/foo/{id}estarían permitidas. La primera es más específica, así que tendría prioridad y por eso parece estar bien.Pero si tienes
/foo/{id}/deletey/foo/bar/{action}, entonces/foo/bar/deletecoincide con ambas y ninguna es más específica que la otra, así que probablemente haría panic. Se siente razonable, aunque quizá sería mejor una prioridad donde gane la primera registrada.$al final.El proyecto gorilla/mux es un poco raro y confuso. El año pasado los mantenedores archivaron el proyecto, así que me pasé a otro multiplexor llamado gin-gonic.
Pero como se mencionó en el artículo, volví a revisar y ya no estaba archivado. No sé si eso pone en duda la estabilidad del proyecto en general, o si más bien es una prueba de que el open source es realmente estable porque, incluso si se van desarrolladores, otras personas pueden aparecer y continuar el mantenimiento. De todos modos, es bueno que esta funcionalidad la ofrezca Go mismo.
[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 ya escribí antes en un comentario[1], creo que la sintaxis de la propuesta está mal
Hay que crear cadenas mágicas raras para definir handlers. No entiendo por qué no lo hacen con argumentos reales; así también se podrían usar más fácilmente las constantes existentes
[1] https://github.com/golang/go/issues/61410#issuecomment-16580...
Si agregaran un nuevo argumento a la interfaz Mux, se rompería el código existente, y como no pueden cambiar la firma del método, estas cadenas mágicas parecen un compromiso razonable. Los métodos HTTP tampoco van a cambiar pronto, y es fácil validarlos al momento de registrar o con análisis estático. No veo bien qué valor aporta usar constantes, sobre todo si eso implica romper la compatibilidad hacia atrás o tener que mantener para siempre en paralelo una nueva API de mux apenas distinta de la API existente
{parameter}, ¿no es ya una cadena mágica?En términos de RFC2616, se puede ver como un
Request-URIcon un poco de magia de parámetros, y pensar que a eso se le suma algo como una línea de solicitud:[ Method SP ] Request-URI. Es totalmente compatible hacia atrás, y aun sin ver la documentación queda bastante claro qué haceMethodes una de algunas constantes predefinidas oextension-method = token, ytokenexcluye espacios, barras y varios tipos de paréntesis, así que incluso con métodos personalizados muy raros no parece probable que haya confusión o que se parsee mal.Get()y.Post()nunca me resolvieron nada. Un único método de registro que ponga el método dentro de la cadena probablemente esté perfectamente bienHEADNo me gusta este enfoque. Me pregunto si hay alguna razón para usar un prefijo de método convertido en cadena
La seguridad de tipos de métodos por verbo como
mux.Getomux.Postes mejor que cadenas mágicas validadas en runtime. También permite autocompletado del editor o IntelliSensefunc Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }Omití los tipos para que se vea simple
Interesante. Ahora que el mux también puede hacer match por método, me preguntaba qué pasa cuando la ruta coincide pero el método no. La cuestión es si devuelve 404 o 405
Al revisar, devuelve 405 con el header
Allowcorrectamente completadohttps://cs.opensource.google/go/go/+/master:src/net/http/ser...
Sé que a mucha gente no le gusta la interfaz basada en strings, pero creo que es más probable escribir correctamente algo equivocado que cometer un typo en el nombre de un método HTTP. Personalmente, me basta con que el análisis estático advierta sobre sintaxis incorrecta
Dicho eso, si ya tienes requisitos avanzados, creo que es mejor no usar el serve mux básico. Hay muchas opciones que se ajustan mejor a distintos casos de uso, y si ya estás generando rutas dinámicamente, quizá sea menos trabajo usar tu propio router que forzar tu estructura de datos a encajar con un router existente
Para rutas superpuestas, sería mucho mejor que hiciera match en el orden en que fueron definidas en vez de hacer panic
En el ejemplo, si se definiera la ruta
/task/0/{action}/antes que la ruta con wildcard, esperaría que esa hiciera match primero. Así sería fácil definir handlers para casos especialesPrefiero que simplemente funcione como dije antes que fallar de una forma “útil”. Huele un poco raro y no muy propio de Go
El ServeMux original fue diseñado para no respetar el orden de registro, porque depender del orden de registro es riesgoso. Uno de los objetivos centrales de diseño de Go es soportar la “programación a escala”, y los efectos secundarios extraños causados por modificaciones en código lejano son exactamente el tipo de cosas que se quieren evitar
En un ejemplo simple donde todos los registros se hacen en una sola función de un solo paquete, la intención del orden de registro es clara. Pero en una base de código suficientemente grande, es probable que los registros ocurran en varios lugares, o que sean resultado de archivos de entrada o de código generado. Quien escribe la especificación del código generado quizá no conozca las sutilezas del orden de ejecución ni cómo afectan al routing. Incluso un cambio en el orden lexicográfico de los imports de paquetes podría cambiar inesperadamente el routing
Evitar resultados inesperados, facilitar refactorizaciones y hacer más fácil razonar sobre programas complejos es claramente una dirección propia de Go. No tienes que estar siempre de acuerdo con la elección, pero la razón es consistente
Me gusta que se reduzca una dependencia externa. Como comentario lateral, la combinación sqlc + Postgres + templ + htmx + Tailwind resultó ser un stack de altísima productividad para desarrollo. templ se siente como JSX para Go
También debería echarle un vistazo a htmx
Esperaba que bud con compilación de Svelte cubriera este hueco, pero templ parece una alternativa mejor
Esta propuesta de verdad no me gusta. ¿Meter el método de solicitud HTTP dentro del URI, y además solo a veces, y quizá también hacer algo como
POST,PUT,PATCH /something? Prefiero pasarSi de verdad hay que hacerlo, bastaría con crear un método dedicado que reciba el nombre del método de solicitud HTTP
Me pregunto qué problema resuelve un método dedicado que recibe el nombre del método HTTP, en comparación con incluir el método en la cadena
Podrían haber puesto un argumento separado para el método o la lista de opciones
Go no es mi herramienta principal, pero he usado Gin, y es así:
router.GET("/", func(context *gin.Context) { ... })No parece muy distinto de muchos otros lenguajes y frameworks. Me pregunto si hay más ejemplos que usen el estilo
GET /path/Que el equipo oficial del lenguaje use cadenas con prefijo en lugar de enumeraciones parece una decisión de diseño muy extraña
Por ejemplo,
(Http.GET, "/path")se ve mejor que el actual("GET /path"). Es raroAun así, preferiría tener varias funciones como
mux.GET,mux.POST, antes que ese enfoque