1 puntos por GN⁺ 2023-10-17 | 1 comentarios | Compartir por WhatsApp
  • 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

 
GN⁺ 2023-10-17
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/bar debía ser una ruta separada y /foo/{id} otra ruta distinta.

    • No se puede garantizar que el orden de registro sea siempre el mismo, así que en la práctica se vuelve comportamiento indefinido. ServeMux fue diseñado e implementado originalmente de esa forma, y se consideró útil seguir soportando ese comportamiento.
      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 Handle o HandleFunc. 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
    • La razón para usar un lenguaje compilado y tipado como Go es mover los bugs más hacia la izquierda. Es mejor detectar un bug al iniciar la aplicación que verlo explotar en producción en un caso raro, y es mejor detectarlo en compilación que al iniciar.
      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.
    • El panic en sí es una herramienta. Impide usar código ambiguo y difícil de razonar, y evita depender de factores que pueden cambiar arbitrariamente, como el orden de inicialización. Por ejemplo, solo cambiar el nombre de un archivo puede afectar el orden de las funciones init por archivo en Go.
      Si necesitas un comportamiento como /foo/bar y /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.
    • Creo que /foo/bar y /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}/delete y /foo/bar/{action}, entonces /foo/bar/delete coincide 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.
    • Para depurar, es mucho mejor recibir un error que obtener datos incorrectos porque la ruta que realmente se ejecutó fue otra. Me pasó algo así en Django por una ruta a la que le faltaba la expresión regular $ 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.

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

    • Supongo que eso es por la garantía de compatibilidad hacia atrás de Go
      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
    • Por el parseo de {parameter}, ¿no es ya una cadena mágica?
      En términos de RFC2616, se puede ver como un Request-URI con 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é hace
      Method es una de algunas constantes predefinidas o extension-method = token, y token excluye 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
    • Estoy de acuerdo en que es un poco raro, pero la razón es clara: no quieren romper ni cambiar interfaces públicas existentes
    • Al principio tuve la misma impresión, pero honestamente, cuando uso Gin, métodos como .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 bien
    • No entiendo cuál es el problema de registrar automáticamente la ruta HEAD
  • No 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.Get o mux.Post es mejor que cadenas mágicas validadas en runtime. También permite autocompletado del editor o IntelliSense

    • Algo así se puede agregar muy fácilmente. Si de verdad te preocupa tanto, puedes hacer algo como func Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }
      Omití los tipos para que se vea simple
    • A mí tampoco me gusta. Quiero tener la certeza de que el routing funciona en tiempo de compilación, no en runtime
    • Si quieres seguridad de tipos, elige un lenguaje con seguridad de tipos
    • No es que prefiera usar cadenas, pero siendo justos, cuando llega una solicitud, el método HTTP al final también es una cadena. Tiene cierta elegancia que coincida con el prefijo de la primera línea del paquete HTTP
    • Esto se trata ampliamente en la propuesta
  • 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 Allow correctamente completado
    https://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 especiales
    Prefiero que simplemente funcione como dije antes que fallar de una forma “útil”. Huele un poco raro y no muy propio de Go

    • La semántica del lenguaje importa, y esto en realidad es una elección bastante propia 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

    • Hasta ahora he tenido una experiencia bastante buena usando sqlc
      También debería echarle un vistazo a htmx
    • Me da curiosidad cómo manejan Tailwind en esto. La idea de agregar Tailwind es muy buena, pero si hay que usar npm, preferiría escribir CSS directamente
    • Al principio pensé que era un typo de template, pero en realidad era un paquete llamado templ, distinto de la biblioteca estándar. Miré un poco la documentación y se ve bastante limpio
      Esperaba que bud con compilación de Svelte cubriera este hueco, pero templ parece una alternativa mejor
    • Sería genial que templ tuviera soporte en IntelliJ. Me gusta la idea
  • 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 pasar
    Si de verdad hay que hacerlo, bastaría con crear un método dedicado que reciba el nombre del método de solicitud HTTP

    • No entiendo por qué. En la práctica no cambia mucho. No creo que se pueda odiar toda la propuesta solo por una cuestión de estilo
      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
    • De acuerdo. Diseñarlo así no tiene mucho sentido
      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/

    • Probablemente casi no haya, porque no hay razón para hacerlo así salvo por compatibilidad hacia atrás
    • En la práctica no lo he visto. Simplemente es una mala forma de hacerlo
  • 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 raro

    • Es una forma de respetar la promesa de compatibilidad de Go 1
    • Como desarrollador de Go, en estos casos me gustaría que existiera la sobrecarga de funciones por compatibilidad hacia atrás. La promesa de compatibilidad es excelente, pero las rarezas que genera no tanto
    • Es un error que no se puede corregir sin romper código existente o introducir funciones nuevas
      Aun así, preferiría tener varias funciones como mux.GET, mux.POST, antes que ese enfoque