3 Min. Lesezeit
Kein Router-Framework
Die Go-Standardbibliothek routet HTTP gut genug
Teil zwei des Boring Stack. In Teil eins ging es um die Sprache. In diesem geht es um die erste Abhängigkeit, die die meisten Go-Web-Apps dazunehmen, und warum ich es nicht tue.
Jahrelang war das Erste in einem Go-Webprojekt die Wahl eines Routers: gorilla/mux, chi, Gin, Echo. Der Standard-http.ServeMux konnte weder nach HTTP-Methode unterscheiden noch Parameter aus dem Pfad lesen, also brauchte man etwas darüber.
Mit Go 1.22 hat sich das geändert. Seither kann die Standardbibliothek beides:
mux := http.NewServeMux()
mux.HandleFunc("GET /items", h.ListItems)
mux.HandleFunc("GET /items/{id}", h.ShowItem)
mux.HandleFunc("POST /items/{id}/archive", h.ArchiveItem)
Im Handler liefert r.PathValue("id") den Parameter. Anfragen mit der falschen Methode bekommen automatisch ein 405. Das deckt alles ab, was ich von einem Router brauche.
Alle Routen in einer Datei
In meinen Apps stehen alle Routen in einer Datei, internal/routes/routes.go, gruppiert in öffentliche Routen, Routen hinter einem Login und Webhooks. Wenn ich wissen will, was eine App kann, lese ich eine Datei von oben nach unten.
Die Authentifizierung wird pro Route angehängt, nicht über Gruppen:
mux.Handle("GET /settings", middleware.RequireAuth(h.Settings))
mux.Handle("GET /admin/users", middleware.RequireAdmin(h.Users))
mux.Handle("GET /login", middleware.RequireGuest(h.LoginForm))
Das ist mehr Tipparbeit als eine Routengruppe. Dafür kann man es nicht falsch lesen: Der Schutz steht auf derselben Zeile wie die Route.
Middleware ist nur eine Funktion
Frameworks lassen Middleware besonders aussehen. Sie ist es nicht. In Go ist Middleware eine Funktion, die einen http.Handler bekommt und einen zurückgibt. Sie zu verketten ist eine Schleife.
Meine Apps schicken jede Anfrage durch dieselbe Kette, in fester Reihenfolge: Konfiguration, eine Nonce für die Content Security Policy, Security-Header, Request-Logging, CSRF-Schutz, Authentifizierung, Sprache und der aktuelle URL-Pfad. Die Reihenfolge zählt. CSRF muss laufen, bevor etwas den Zustand ändert, und die Sprache muss wissen, wer der Nutzer ist. Als normale Funktionen geschrieben, ist die Reihenfolge an einem Ort sichtbar, und es passiert nichts, was ich nicht selbst geschrieben habe.
Request-Daten über den Context
Handler und Templates müssen Dinge über die Anfrage wissen: den angemeldeten Nutzer, das CSRF-Token, die Sprache. Frameworks bringen dafür eigene Context-Objekte mit. Die Standardbibliothek hat schon eines: context.Context.
Alle Werte einer Anfrage liegen bei mir in einem kleinen Paket mit typisierten Zugriffsfunktionen wie ctxkeys.User(ctx). Das ist der einzige Weg, auf dem sie Handler und Templates erreichen. Wenn etwas den Nutzer liest, weiss ich genau, woher er kommt.
Was es kostet
- Etwas mehr Code. Keine Routengruppen, kein automatisches Binden von Formularwerten an Structs, keine eingebaute Validierung. Ich schreibe ein paar Zeilen mehr, und jede davon kann ich selbst lesen.
- Kein Plugin-Ökosystem. Ich vermisse es nicht. Die Plugins, die ich nutzen würde (Logging, Recovery, CORS, Rate Limiting), sind je ein paar Dutzend Zeilen.
- Es braucht Go 1.22 oder neuer. Das gibt es seit Februar 2024.
Wann ich anders entscheiden würde
Wenn eine bestehende App auf chi oder Echo läuft und funktioniert, gibt es keinen Grund zu migrieren. Bei einem neuen Projekt würde ich ein Router-Framework aber erst dazunehmen, wenn ein konkreter Bedarf auftaucht. Bisher ist er nicht aufgetaucht.
Als Nächstes: wie das HTML in den Browser kommt.