Go-Web-Frameworks 2026: net/http, chi, Gin, Echo und Fiber im Vergleich
Fünf Go-HTTP-Stacks, ein Benchmark-Setup, echte Zahlen.
Go liefert einen produktionsreifen HTTP-Server in seiner Standardbibliothek mit, und seit Go 1.22 übernimmt der eingebaute Router selbstständig die Methodenabgleichung und Pfadparameter. Die Frage nach dem Framework hat sich verengt, ist aber nicht verschwunden.
Fünf Stacks decken nahezu jede Go-Backend-Entscheidung in 2026 ab: die Standardbibliothek net/http, chi, Gin, Echo und Fiber. Jeder nimmt eine andere Position im Hinblick auf denselben Kompromiss ein: Was die Bibliothek einem gibt, im Verhältnis zu dem, was man ihr an Abhängigkeiten, Konventionen und Ökosystem-Kompatibilität schuldet. Dieser Artikel vergleicht sie anhand von Routing-Leistungsfähigkeit, Handler-Ergonomie, Middleware und gemessener Leistung und endet mit einer Entscheidungstabelle statt einem einzelnen Urteil.
Wer bei Null anfangen und den kompletten Build-Prozess um seine Wahl herum durchlaufen möchte – Validierung, Authentifizierung, Tests, Bereitstellung –, findet die Schritt-für-Schritt-Version in REST-APIs in Go bauen. Hier liegt der Fokus auf den Stacks selbst.

Der ServeMux-Neustart nach 1.22
Der Grund, warum sich die Diskussion über Frameworks verändert hat, ist eine Ergänzung in der Standardbibliothek. Go 1.22 verschaffte http.ServeMux methodenbewusste Muster und Pfadparameter:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// ...
})
mux.HandleFunc("POST /users", createUser)
Vor diesem Update erforderte jede API, die GET /users/{id} benötigte, einen Router von Dritten, wodurch Gin, chi und Echo zu den Standards wurden. Im Jahr 2026 deckt die Standardbibliothek statische Routen, Methodenabgleichung, Ein-Segment- und Wildcard-Parameter sowie Middleware durch einfaches Funktions-Wrapping ab. Was sie noch nicht liefert: Routengruppen, komponierbare Middleware-Ketten, Struktur-Binding oder pro-Routen-Parametertypen.
Die Kontrahenten
| net/http (Standardbibliothek) | chi v5.3.2 | Gin v1.12.0 | Echo v4.16.0 | Fiber v2.52.15 | |
|---|---|---|---|---|---|
| HTTP-Engine | net/http | net/http | net/http | net/http | fasthttp |
| Handler-Signatur | func(w, r) |
func(w, r) |
func(c *gin.Context) |
func(c echo.Context) error |
func(c *fiber.Ctx) error |
http.Handler-kompatibel |
ja | ja | ja | ja | nein |
| Middleware-Ökosystem | wachsend, Standardbibliotheks-geprägt | Standardbibliotheks-geprägt | größtes, Gin-spezifisch | groß, Echo-spezifisch | wachsend, fasthttp-spezifisch |
| Binding/Validierung | manuell | manuell | eingebaute Tags | eingebaut | über Hilfsfunktionen |
| HTTP/2 | ja | ja | ja | ja | nein |
| Abhängigkeiten | 0 | 1 | ~6 | ~8 | ~4 |
Alle fünf verwenden Trie- oder Radix-basierte Router mit Pfadparametern und gruppierten Routen. Die strukturelle Trennlinie liegt in der letzten Zeile der Geschichte über die Handler-Signatur: chi und die Standardbibliothek sprechen reines http.Handler; Gin, Echo und Fiber kapseln Anfragen in ihrem eigenen Kontexttyp. Fiber ist erneut der Außenseiter, da er auf fasthttp basiert und nicht auf net/http.
Was ich gemessen habe
Anstatt synthetische Router-Benchmarks zu zitieren, habe ich denselben Service auf allen fünf Stacks ausgeführt. Das Messaufbau-Setup:
- Go 1.27.1, Gin v1.12.0, chi v5.3.2, Echo v4.16.0, Fiber v2.52.15
- Intel Core Ultra 7 265, Linux, 20 Threads, localhost, kein TLS
- Drei Routen pro Stack:
GET /health,GET /users/{id},GET /api/v1/users/{uid}/orders/{oid} - Last: 8 Keep-Alive-Worker für 3 Sekunden gegen die Route mit zwei Parametern, nach einer Warmup-Passage
- Jeder Handler serialisiert pro Anfrage ein kleines JSON-Dokument mit
encoding/json– die Zahlen umfassen also Serialisierung, nicht nur Routing
Die Standardbibliotheks-Version der Hot-Route:
mux.HandleFunc("GET /api/v1/users/{uid}/orders/{oid}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write(orderBody(r.PathValue("uid"), r.PathValue("oid")))
})
und das Fiber-Äquivalent:
app.Get("/api/v1/users/:uid/orders/:oid", func(c *fiber.Ctx) error {
c.Set("Content-Type", "application/json")
return c.Send(orderBody(c.Params("uid"), c.Params("oid")))
})
Zwei Hinweise zur Ehrlichkeit vor den Zahlen. Erstens ist dies eine localhost-Mikrolast: keine Datenbank, kein TLS-Handshake, keine Middleware-Kette. Reale Dienste stoßen bei der Eingabe/Ausgabe an Grenzen – der Anteil des Frameworks an einer Anfrage, die PostgreSQL über GORM, Ent, Bun oder sqlc abfragt, ist ein Rundenfehler. Zweitens erhielt jeder Stack seinen eigenen Serverprozess und eine identische Handler-Form, sodass der Vergleich zwischen Stacks und nicht zwischen manuell optimierten Konfigurationen liegt.
Ergebnisse
Durchsatz auf der dynamischen Route mit zwei Parametern:
| Stack | Anfragen/Sekunde | Durchschnittliche Latenz |
|---|---|---|
| Fiber (fasthttp) | 205.958 | 0,034 ms |
| Echo | 169.826 | 0,042 ms |
| Gin | 169.715 | 0,042 ms |
| net/http ServeMux | 169.708 | 0,042 ms |
| chi | 163.017 | 0,044 ms |
Die vier auf net/http basierenden Stacks liegen innerhalb von 4 % voneinander. Die fasthttp-Engine von Fiber ist bei dieser localhost-Last etwa 21 % voraus – real, aber die Art von Lücke, die ein einzelner Datenbank-Roundtrip (0,5–5 ms) in der Produktion irrelevant macht.
Um zu sehen, wo die Kosten pro Anfrage tatsächlich hinführen, habe ich auch die Handler-Aufrufe ohne Netzwerkkomponente gemessen, indem ich jeden Router direkt über http.Handler.ServeHTTP aufrief (Fiber über seinen fasthttp-Handler):
| Stack | Zeit/Operation | Bytes/Operation | Allokationen/Operation |
|---|---|---|---|
| Gin | 289 ns | 192 B | 3 |
| Echo | 333 ns | 192 B | 3 |
| net/http ServeMux | 430 ns | 240 B | 5 |
| Fiber | 541 ns* | 192 B | 3 |
| chi | 592 ns | 897 B | 7 |
* Die Fiber-Zahl enthält Harness-Overhead – der Test muss die Anfrage jede Iteration in den wiederverwendbaren Kontext von fasthttp kopieren, was der echte Server umgeht, indem er direkt vom Draht liest. Man sollte dies als obere Grenze betrachten; der serverseitige Durchsatz oben ist der faire Vergleich für Fiber.
Das Allokationsprofil ist die nachhaltigere Geschichte. Gin und Echo poolen ihre Anfragekontexte, sodass eine Anfrage über sie 3 Allokationen kostet. Die 5 Allokationen der Standardbibliothek gehen auf die eigentliche, anfrageweise Verwaltung zurück. chi speichert Routenparameter in context.Context der Anfrage, was sich in 897 B und 7 Allokationen pro Anfrage niederschlägt – der Preis dafür, ein reines http.Handler mit reichen Routeninformationen zu bleiben.
Fiber und der fasthttp-Kompromiss
Fiber ist der schnellste Stack im Server-Benchmark, und die Geschwindigkeit kommt mit einer Kompatibilitätsrechnung. fasthttp ist eine HTTP-Implementierung von Grund auf mit eigenen Anfrage-/Antworttypen, daher:
net/http-Middleware lässt sich nicht anhängen. Jede Bibliothek, die einhttp.Handleroder eine*http.Requesterwartet, benötigt eine Konvertierung oder ein Fiber-natives Port.- Kein HTTP/2. fasthttp implementiert dies nicht; Go’s
net/httperhält h2 über TLS kostenlos aus der Standardbibliothek. - WebSocket, Streaming und
http.Flusher-ähnliche Tricks nehmen alle den fasthttp-Weg, der sich vom Standardbibliotheks-Verhalten unterscheidet, das die meisten Go-Dokumentationen voraussetzen.
Was Fiber im Gegenzug kauft: die schnellsten Zahlen in der Tabelle oben, eine Express-ähnliche API und niedrigere Allokationen als die Framework-Benchmarks nahelegen, sobald der Vorteil beim Draht-Parsing berücksichtigt wird. Es passt, wenn Antworten klein sind, Routing dominiert, HTTP/2 upstream terminiert wird und die benötigte Middleware im Fiber-Ökosystem existiert. Diese Bedingungen gelten für einige Edge-Dienste und für fast nichts anderes.
chi: der langweilige Mittelweg
chi ist ein Router und eine Middleware-Pipeline, nichts weiter. Handler sind func(w, r), Middleware ist func(http.Handler) http.Handler, und alles, was für die Standardbibliothek geschrieben wurde, hängt sich ohne Adapter an:
r := chi.NewRouter()
r.Use(RequestID, RealIP, Logger, Recoverer)
r.Route("/api/v1", func(r chi.Router) {
r.Get("/users/{uid}/orders/{oid}", getOrder)
})
Das ist der gesamte Verkaufsargument: Routengruppen und ein Middleware-Stack auf Basis der Standard-Schnittstelle. Die Allokationstabelle preist dies ehrlich an – reicheren Routenkontext pro Anfrage als die Standardbibliothek allein, im Austausch gegen komponierbare Middleware und verschachtelte Routen. Für Dienste, die Struktur ohne Framework-Identität wollen, ist chi plus net/http die Konfiguration, auf die sich die meisten Go-Teams einigen.
Gin vs. Echo, im Code
Gin und Echo sind in der Leistungszu nahezu Zwillinge – innerhalb des Rauschens in beiden Benchmarks –, daher liegt die Wahl zwischen ihnen im API-Geschmack und der Philosophie der Fehlerbehandlung.
Echo zentralisiert Fehler in einem HTTPErrorHandler. Handler geben error zurück, und eine Funktion weist Fehler Antworten zu:
func getOrder(c echo.Context) error {
o, err := store.Order(c.Param("uid"), c.Param("oid"))
if err != nil {
return echo.NewHTTPError(http.StatusNotFound, "order not found")
}
return c.JSON(http.StatusOK, o)
}
Gin-Handler geben nichts zurück; Fehler sammeln sich im Kontext an, und man renderst sie, wo man es für richtig hält:
func getOrder(c *gin.Context) {
o, err := store.Order(c.Param("uid"), c.Param("oid"))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "order not found"})
return
}
c.JSON(http.StatusOK, o)
}
Das Modell von Echo lässt sich sauber auf Go-Fehlerbehandlungsarchitektur mit gewickelten Fehlern und zentraler Grenzbearbeitung abbilden – an einem Ort werden die Ergebnisse von errors.Is in Statuscodes übersetzt. Das Modell von Gin entspricht dem häufigeren Muster, pro Handler zu entscheiden. Neben Fehlern sind die entscheidenden Vorteile von Gin die Größe des Ökosystems und das eingebaute Struktur-Binding mit Validierungs-Tags; bei Echo sind es eine konsistente Kontext-API und stärkere eingebaute Middleware. Beide integrieren die Swagger-Generierung über ihre eigenen Adapter – die einrichtungsbezogene Konfiguration für jedes Framework wird in Swagger zu Ihrer Go-API hinzufügen behandelt.
Eine weitere Achse: Kontext-Propagation. Gin und Echo kapseln context.Context in ihren eigenen Kontexttypen, sodass das Übergeben von Abbrüchen in Handler einen Extraktionsschritt erfordert. Die Muster, um Abbruch, Timeouts und Werte über all diese Wrapper hinweg gerad zu halten, finden sich in Go context.Context richtig gemacht.
Die Timeouts, die niemand setzt
Jeder Stack in diesem Vergleich lässt die Server-Timeouts auf Null-Standardwerte, es sei denn, man setzt sie selbst – und Null bedeutet kein Limit. Ein Client, der eine Verbindung öffnet und nichts sendet, hält einen Goroutine und eine Datei-Deskriptor endlos fest:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
srv.ListenAndServe()
ReadHeaderTimeout ist das Erste, was man setzen sollte – es begrenzt Slowloris-artige Verbindungsermüdung, ohne lange Uploads zu unterbrechen, wie ein pauschales ReadTimeout könnte. WriteTimeout muss die langsamste legitime Antwort abdecken, was es zu einer politichen Entscheidung im Zusammenhang mit den eigenen Timeout-Budgets macht, nicht zu einer Copy-Paste-Konstante. Denselben Disziplin gilt pro Framework: Gin, Echo und Fiber akzeptieren alle serverseitige Timeout-Konfiguration, und keines von ihnen aktiviert sie standardmäßig.
Wählen
oder Null-Abhängigkeits-Anforderung?] B -- ja --> C[net/http ServeMux] B -- nein --> D[Eingebautes Binding,
Validierung, große Middleware-Katalog gewünscht?] D -- nein --> E[chi + net/http] D -- ja --> F[Team-Konvention?] F -- Fehler-returnende Handler --> G[Echo] F -- größtes Ökosystem, Gin-Idiome --> H[Gin] A --> I[Kleine Antworten, extremer Durchsatz,
kein HTTP/2, Upstream-TLS-Terminierung?] I -- alle wahr --> J[Fiber] I -- nein --> E
| Situation | Empfehlung |
|---|---|
| Interner Dienst, CLI-Endpunkt, Plugin, Bibliothek | net/http allein |
| Die meisten APIs – Struktur ohne Framework-Lock-in | chi über net/http |
| Öffentliche API, größeres Team, Validierung und Binding aus der Schachtel | Gin |
| Öffentliche API, zentralisierte Fehlerübersetzung, konsistente Kontext-API | Echo |
| Bewiesener Routing/Serialisierungs-Engpass, winziges JSON, kein HTTP/2 | Fiber |
Wenn der ehrliche Standard für einen neuen Dienst in 2026 benannt werden muss: Beginnen Sie mit net/http, fügen Sie chi hinzu, wenn Middleware oder Routengruppen zu drücken beginnen, und wechseln Sie nur zu Gin oder Echo, wenn Binding, Validierung oder Team-Konvention eine Framework-Identität rechtfertigen. Fiber bleibt ein Spezialwerkzeug – es lohnt sich, es zu benchmarken, wenn der Durchsatz als Problem bewiesen ist, und man den Preis für die Kompatibilitätsrechnung abwägt.
Migration zwischen Stacks
Die Migrationskosten spiegeln die Trennlinie der Handler-Signatur wider. Standardbibliothek ↔ chi ist mechanisch: Handler behalten dieselbe Signatur, Routen wechseln von mux.HandleFunc-Mustern zu r.Get, Middleware hängt sich an den chi-Stack. Gin ↔ Echo ist eine Kontexttyp-Übersetzung – c.Param, c.JSON und Middleware-Logik lassen sich fast zeilenweise portieren. Alles, was zu oder von Fiber migriert wird, ist ein Neuschreiben der HTTP-Schicht, da *http.Request-basierter Code, net/http-Middleware und h2-Voraussetzungen die fasthttp-Grenze nicht überleben; der am wenigsten schmerzhafte Weg ist, die Geschäftslogik in frameworkfreien Paketen zu halten und die Transportschicht neu zu generieren, was auch die Struktur ist, die die nächste Migration günstig macht.