- Die ServeMux-Verbesserungen in
net/http von Go 1.22 bringen stärkeres Pattern Matching in den Standard-HTTP-Multiplexer und verringern damit den Bereich, in dem man wegen einfacher Pfad-Matches auf Third-Party-Router angewiesen war
- Der neue mux kann HTTP-Methoden und Pfad-Wildcards wie
/task/{id}/ im selben Pattern verwenden, sodass derselbe Pfad je nach GET, POST oder DELETE mit unterschiedlichen Handlern verbunden werden kann
- Gematchte Werte lassen sich mit
req.PathValue("id") auslesen; mit {id}... und {$} kann man den gesamten nachfolgenden Pfad bzw. das exakte Pfadende steuern
- Pattern-Konflikte werden beim Registrieren durch eine panic sichtbar und zeigen, welcher Pfad auf beide Patterns matcht und warum keines von beiden spezifischer ist
- Für mehr Server könnte der Standard-mux allein ausreichen, doch Third-Party-Router und Frameworks wie
gorilla/mux oder Gin bieten weiterhin einen größeren Funktionsumfang und mehr Werkzeuge
Änderungen an ServeMux in Go 1.22
- Go 1.22 soll erweitertes Pattern Matching für
http.ServeMux bringen, den standardmäßigen HTTP-Serving-Multiplexer in net/http
- Der bisherige
http.ServeMux war vor allem auf grundlegendes Pfad-Matching ausgerichtet, weshalb für komplexeres Routing häufig Third-Party-Bibliotheken genutzt wurden
- Der neue mux holt fortgeschrittenes Matching in die Standardbibliothek und verkleinert damit die Funktionslücke zu Third-Party-Paketen
- Als Vergleich dienen Beispiele für REST-Server mit dem bisherigen Ansatz der Standardbibliothek und mit
gorilla/mux
Grundlegende Nutzung des neuen mux
- Wer schon Go-Router-Pakete wie
gorilla/mux verwendet hat, wird die Nutzung des neuen Standard-mux vertraut finden
- Das Beispiel erstellt mit
http.NewServeMux() einen mux und registriert reichhaltigere Patterns über 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)
})
- Der erste Handler enthält die Methode
GET im Pattern und funktioniert daher nur für GET-Requests, die mit /path/ beginnen
- Der zweite Handler matcht mit
{id} in /task/{id}/ eine einzelne Pfadkomponente und liest den Wert im Handler mit r.PathValue("id") aus
- Da Go 1.22 zum Zeitpunkt des Beispiels noch nicht veröffentlicht war, wird die Ausführung mit
gotip empfohlen
- Die Testergebnisse machen die Routing-Regeln direkt sichtbar
- Ein Request auf
/what/ ergibt 404 page not found
- Ein
GET-Request auf /path/ ergibt got path
- Ein
POST-Request auf /path/ ergibt Method Not Allowed
- Ein Request auf
/task/f0cd2e/ wird mit id=f0cd2e verarbeitet
Zusätzliche Pattern-Funktionen und Konfliktbehandlung
- Der neue
ServeMux erlaubt neben einfachen Wildcards eine feinere Kontrolle des Pfad-Matchings
- Mit
{id}... wird der gesamte nachfolgende Pfad auf die Wildcard gematcht
- Mit
{$} wird das Pfadende strikt gematcht
- Verarbeitung von Prioritäten und Konfliktregeln zwischen Patterns
- Mögliche Konflikte waren ein besonders beachteter Teil des Vorschlags
- Die folgenden beiden Patterns matchen beispielsweise beide auf einen Request an
/task/0/status/
mux.HandleFunc("/task/{id}/status/", ...)
mux.HandleFunc("/task/0/{action}/", ...)
- In diesem Fall löst der neue
ServeMux beim Registrieren eine panic aus
- Die Fehlermeldung zeigt sowohl einen Beispielpfad, auf den beide Patterns matchen, als auch den Grund, warum keines der Patterns spezifischer ist als das andere
- In komplexem Code, der Patterns an mehreren Stellen registriert, ist eine solche detaillierte Konfliktdiagnose besonders nützlich
Neuimplementierung des task-Server-Beispiels
- Der task/todo-list-Server aus der früheren Reihe REST Servers in Go wurde mit dem verbesserten mux aus Go 1.22 neu implementiert
- Der vollständige Code liegt im stdlib-newmux-Beispiel
- Die registrierten Patterns verwenden HTTP-Methoden zusammen mit Pfad-Wildcards
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)
- Wie im Beispiel mit
gorilla/mux lässt sich derselbe Pfad nach HTTP-Methode getrennt auf unterschiedliche Handler aufteilen
- Beim früheren
http.ServeMux landeten mehrere Methoden für denselben Pfad im selben Handler, und die Methode musste innerhalb des Handlers geprüft werden
- Im neuen mux wird ein größerer Teil der Routing-Entscheidung außerhalb des Handlers erledigt, wodurch sich Handler leichter trennen lassen
Nutzung und Einschränkungen von PathValue
getTaskHandler liest die aus dem Pfad extrahierte ID mit req.PathValue("id") und wandelt sie mit strconv.Atoi in eine Ganzzahl um
- Das
{id}-Pattern des neuen mux gibt keinen regulären Ausdruck vor, der nur Ganzzahlen matcht; daher ist Fehlerbehandlung für strconv.Atoi erforderlich
- Schlägt die ID-Konvertierung fehl, wird mit
http.StatusBadRequest geantwortet; findet der Speicher die task nicht, wird mit http.StatusNotFound geantwortet
- Die finale Implementierung ist der Lösung aus Part 2 mit
gorilla/mux sehr ähnlich
- Falls nötig, lässt sich der nachfolgende Pfad mit der
{$}-Wildcard des neuen http.ServeMux einfach einschränken
Position von Standard-mux und Third-Party-Routern
- Für Go-Anfänger war „Welches Router-Paket soll ich verwenden?“ eine häufige Frage
- Nach Go 1.22 könnten viele Nutzer feststellen, dass der neue Standard-mux auch ohne Third-Party-Pakete ausreicht
- Router wie
gorilla/mux bieten weiterhin mehr Funktionen als die Standardbibliothek
- Leichtgewichtige Frameworks wie Gin bieten neben einem Router auch zusätzliche Werkzeuge für den Aufbau von Web-Backends
- Dass die Standardbibliothek leistungsfähiger wird, ist sowohl für Nutzer von Third-Party-Paketen als auch für Nutzer, die nur die Standardbibliothek verwenden, eine positive Entwicklung
1 Kommentare
Meinungen auf Hacker News
Einen panic auszulösen, wenn zwei Routen gleichzeitig matchen, wirkt kontraintuitiv. Andere Web-Frameworks verwenden normalerweise die zuerst registrierte passende Route; ich frage mich, ob es einen Grund gibt, warum das nur bei Go anders ist.
Der Randfall, dass „HTTP-Routen an mehreren Stellen registriert werden können und doppelte Matches deshalb schwer zu finden sind“, scheint sich mit Werkzeugen lösen zu lassen. In meiner ganzen Laufbahn habe ich das Verhalten „der erste Match gewinnt“ oft genutzt, und es gab viele fachliche Anforderungen, bei denen
/foo/bareine eigene Route sein musste und/foo/{id}eine andereIm Design Proposal[1] heißt es, dass das Matchen des spezifischeren Patterns die Reihenfolge-Unabhängigkeit der ursprünglichen ServeMux-Patterns bewahren kann, es aber auf einen Blick schwer zu erkennen sein kann, welches von zwei Patterns spezifischer ist oder warum sie kollidieren. Deshalb zeigt die panic-Meldung beim Registrieren kollidierender Patterns die Kollision anhand eines Beispielpfads.
Auch in der Hintergrunddiskussion[2] wird erklärt, dass die mux-Semantik nicht von der Aufrufreihenfolge von
HandleoderHandleFuncabhängt. Dank der Reihenfolge-Unabhängigkeit ist die Initialisierungsreihenfolge von Paketen nicht wichtig, und Code-Refactorings werden einfacher. Deshalb wurde als Kriterium für Gleichstände nicht die Registrierungsreihenfolge gewählt; doppelte Registrierungen lösen einen panic aus, und das Vermeiden einer Semantik, die von der Registrierungsreihenfolge abhängt, bleibt ein zentrales Designziel.[1]: https://github.com/golang/go/issues/61410
[2]: https://github.com/golang/go/discussions/60227
In einem aktuellen persönlichen Projekt bin ich bei der Nutzung von gorilla/mux auf einen Bug gestoßen, bei dem ich versehentlich überlappende Routen erstellt hatte. Hätte der Router statt der Ausführung einen panic ausgelöst, hätte mich das dazu gebracht, die URLs zu refactoren, und der Bug wäre vollständig vermieden worden
init-Funktionen in Go beeinflussen.Wenn man ein Verhalten wie
/foo/barund/foo/{id}braucht, kann man einen Handler für den Basispfad erstellen und darin explizit auf andere Handler-Funktionen verzweigen. Wenn man ein Verhalten möchte, das es in der Standardbibliothek nicht gibt, gibt es auch viele andere Bibliotheken mit mehr Funktionen.Die Go-Standardbibliothek und die Sprache selbst haben sich angesichts möglicher Mehrdeutigkeit immer eher konservativ verhalten. Dadurch ist es leicht, eine starke und präzise Intuition dafür zu entwickeln, wie die Standardbibliothek funktioniert. Code schnell und korrekt lesen und verstehen zu können, ist viel wertvoller, als beim Schreiben ein paar Sekunden zu sparen
/foo/barund/foo/{id}dürften wohl erlaubt sein. Ersteres ist spezifischer und hat deshalb Priorität, also scheint das in Ordnung zu sein.Wenn es allerdings
/foo/{id}/deleteund/foo/bar/{action}gibt, matcht/foo/bar/deleteauf beide, und keine von beiden ist spezifischer; daher dürfte es einen panic geben. Das fühlt sich vernünftig an, aber eine Priorität, bei der die zuerst registrierte Route gewinnt, könnte auch besser sein$-Regex fehlteDas Projekt gorilla/mux ist etwas seltsam und verwirrend. Letztes Jahr haben die Maintainer das Projekt archiviert, deshalb bin ich zu einem anderen Multiplexer namens gin-gonic gewechselt.
Als es im Artikel erwähnt wurde, habe ich noch einmal nachgesehen, und es war nicht mehr archiviert. Ich weiß nicht, ob das Zweifel an der Stabilität des gesamten Projekts aufwirft oder ob es eher ein Beleg dafür ist, dass Open Source tatsächlich stabil ist, weil andere die Wartung übernehmen können, wenn Entwickler aussteigen. Trotzdem ist es gut, dass diese Funktion in Go selbst bereitgestellt wird
[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
Wie ich schon früher in einem Kommentar[1] geschrieben habe: Ich finde, die Syntax des Vorschlags ist falsch.
Um Handler zu definieren, muss man merkwürdige magische Strings bauen. Ich verstehe nicht, warum man daraus keine echten Argumente macht; dann könnte man auch bestehende Konstanten leichter verwenden.
[1] https://github.com/golang/go/issues/61410#issuecomment-16580...
Wenn man dem Mux-Interface ein neues Argument hinzufügt, bricht bestehender Code; und weil man die Methodensignatur nicht ändern kann, wirken solche magischen Strings wie ein vernünftiger Kompromiss. HTTP-Methoden werden sich auch nicht gerade bald ändern, und zur Registrierungszeit oder per statischer Analyse lässt sich das leicht validieren. Ich sehe nicht recht, welchen Wert Konstanten bringen sollen – erst recht nicht, wenn man dafür Abwärtskompatibilität bricht oder für immer eine neue Mux-API parallel pflegen muss, die sich nur minimal von der bestehenden unterscheidet.
{parameter}ist es doch ohnehin schon ein magischer String, oder?In RFC2616-Begriffen kann man es als
Request-URImit etwas Parameter-Magie sehen, und wenn man sich vorstellt, dass daraus wie bei einer Request-Line[ Method SP ] Request-URIwird, passt das. Es ist vollständig abwärtskompatibel, und auch ohne Doku ist ziemlich klar, was es macht.Methodist entweder eine von ein paar vordefinierten Konstanten oderextension-method = token, undtokenschließt Leerzeichen, Slashes und diverse Klammern aus. Daher scheint es selbst bei sehr seltsamen Custom-Methoden kaum möglich, dass etwas verwechselt oder falsch geparst wird..Get()oder.Post()bei der Nutzung von Gin nie wirklich etwas gelöst. Eine einzelne Registrierungsmethode, bei der die Methode im String steckt, ist wahrscheinlich völlig in Ordnung.HEAD-Routen automatisch zu registrieren.Mir gefällt dieser Ansatz nicht. Ich frage mich, ob es einen Grund gibt, warum man stringifizierte Methoden-Präfixe verwendet.
Typsicherheit durch verb-spezifische Methoden wie
mux.Getodermux.Postist besser als magische Strings, die erst zur Laufzeit validiert werden. Außerdem ermöglicht das Editor-Autovervollständigung bzw. IntelliSense.func Get(mux, uri, handler) { mux.HandleFunc("GET " + uri, handler) }bauen.Die Typen habe ich nur der Kürze halber weggelassen.
Interessant. Da der Mux nun auch Methoden matchen kann, habe ich mich gefragt, was passiert, wenn die Route passt, aber die Methode nicht. Also ob es 404 oder 405 ist.
Wie sich herausstellt, wird ein 405 zurückgegeben, bei dem der
Allow-Header korrekt gefüllt ist.https://cs.opensource.google/go/go/+/master:src/net/http/ser...
Ich weiß, dass viele Leute stringbasierte Interfaces nicht mögen, aber ich halte es für wahrscheinlicher, dass man korrekt etwas Falsches eingibt, als dass man den Namen einer HTTP-Methode vertippt. Für mich persönlich reicht es, wenn statische Analyse bei falscher Syntax warnt.
Wenn man allerdings bereits fortgeschrittene Anforderungen hat, sollte man meiner Meinung nach eher nicht den Standard-Serve-Mux verwenden. Es gibt viele Optionen, die besser zu unterschiedlichen Use Cases passen, und wenn man bereits dynamisch Routen erzeugt, kann es weniger Aufwand sein, gleich einen eigenen Router zu verwenden, statt die eigene Datenstruktur in einen bestehenden Router hineinzuzwingen.
Bei überlappenden Pfaden wäre es viel besser, wenn sie nicht panicen, sondern in der definierten Reihenfolge gematcht würden.
Wenn man im Beispiel die Route
/task/0/{action}/vor der Wildcard-Route definiert, würde ich erwarten, dass sie zuerst matcht. So könnte man Handler für Sonderfälle leicht definieren.Statt auf eine „hilfreiche“ Weise zu scheitern, wäre es besser, einfach so zu funktionieren, wie ich es gesagt habe. Das riecht etwas merkwürdig und nicht sehr Go-typisch.
Der ursprüngliche ServeMux wurde bewusst so entworfen, dass er die Registrierungsreihenfolge nicht respektiert, weil es gefährlich ist, sich darauf zu verlassen. Eines der zentralen Designziele von Go ist die Unterstützung von „Programmieren im großen Maßstab“, und seltsame Nebeneffekte durch weit entfernte Codeänderungen sind genau die Art von Dingen, die man vermeiden will.
In einem einfachen Beispiel, in dem alle Registrierungen in einer einzigen Funktion eines einzigen Pakets erfolgen, ist die Absicht der Registrierungsreihenfolge klar. In einer hinreichend großen Codebasis können Registrierungen aber an mehreren Stellen stattfinden und auch aus Eingabedateien oder generiertem Code stammen. Wer die Spezifikation für generierten Code schreibt, kennt möglicherweise nicht die Feinheiten der Ausführungsreihenfolge und deren Auswirkungen auf das Routing. Schon eine Änderung der lexikografischen Sortierung von Package-Imports könnte das Routing unerwartet verändern.
Unerwartete Ergebnisse zu vermeiden, Refactoring zu erleichtern und komplexe Programme leichter nachvollziehbar zu machen, ist eindeutig eine Go-typische Richtung. Man muss der Entscheidung nicht immer zustimmen, aber die Begründung ist konsistent.
Schön, dass eine externe Abhängigkeit wegfällt. Nebenbei: Die Kombination sqlc + Postgres + templ + htmx + Tailwind war ein Stack mit sehr hoher Entwicklungsproduktivität. templ fühlt sich wie JSX für Go an.
htmx sollte ich mir auch einmal ansehen.
Ich hatte gehofft, dass bud mit angehängter Svelte-Kompilierung diese Lücke füllen würde, aber templ scheint eine bessere Alternative zu sein.
Dieser Vorschlag gefällt mir wirklich nicht. Die HTTP-Request-Methode in die URI zu packen – und das auch nur manchmal – und dann vielleicht sogar so etwas wie
POST,PUT,PATCH /somethingzu machen, darauf würde ich gern verzichten.Wenn es unbedingt sein muss, kann man eine eigene Methode erstellen, die den Namen der HTTP-Request-Methode entgegennimmt.
Mich würde interessieren, welches Problem eine eigene Methode, die den Namen der HTTP-Methode entgegennimmt, im Vergleich dazu löst, die Methode in den String aufzunehmen.
Man hätte separate Argumente für die Methode oder die Liste von Optionen vorsehen können.
Go ist nicht mein Hauptwerkzeug, aber ich habe Gin benutzt, und dort sieht es so aus:
router.GET("/", func(context *gin.Context) { ... })Das scheint sich nicht groß von vielen anderen Sprachen und Frameworks zu unterscheiden. Ich frage mich, ob es weitere Beispiele gibt, die den Ansatz
GET /path/verwenden.Dass ein offizielles Sprach-Team präfixierte Strings statt Enums verwendet, wirkt wie eine sehr seltsame Designentscheidung.
Zum Beispiel scheint
(Http.GET, "/path")besser zu sein als das aktuelle("GET /path"). Merkwürdig.Trotzdem würde ich mehrere Funktionen wie
mux.GETundmux.POSTdieser Methode vorziehen.