Frameworki Go w 2026 roku: porównanie net/http, chi, Gin, Echo i Fiber
Pięć stosów HTTP od Go, jedna platforma testowa, realne dane.
Język Go zawiera w swojej standardowej bibliotece produkcyjnie gotowy serwer HTTP, a od wersji Go 1.22 wbudowany router sam radzi sobie z dopasowaniem metod i parametrów ścieżki. Pytanie o wybór frameworku się zawęziło, ale nie zniknęło.
Pięć ekosystemów pokrywa niemal każdą decyzję dotyczącą backendu w Go w 2026 roku: net/http ze standardowej biblioteki, chi, Gin, Echo oraz Fiber. Każdy z nich zajmuje inne stanowisko wobec tego samego kompromisu — tego, co biblioteka daje, w zamian za zależności, konwencje i kompatybilność z ekosystemem. W tym artykule porównujemy je pod kątem mocy routingu, ergonomii handlerów, middleware oraz zmierzonych wyników wydajnościowych, kończąc tabelą decyzyjną, a nie jednym werdyktem.
Jeśli zaczynasz od zera i chcesz pełnego procesu budowy wokół swojego wyboru — walidacja, uwierzytelnianie, testowanie, wdrażanie — wersja krok po kroku znajduje się w artykule Budowanie API REST w Go. Tutaj skupiamy się na samych ekosystemach.

Reset ServeMux po wersji 1.22
Powodem zmiany dynamiki debaty o frameworkach jest jedno dodanie do standardowej biblioteki. Go 1.22 nadało http.ServeMux wzorce z uwzględnieniem metod i parametry ścieżki:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// ...
})
mux.HandleFunc("POST /users", createUser)
Przed tą wersją każde API wymagające GET /users/{id} potrzebowało routera zewnętrznego, co sprawiło, że Gin, chi i Echo stały się standardem. W 2026 roku standardowa biblioteka pokrywa statyczne trasy, dopasowanie metod, parametry pojedynczych segmentów i dzikie znaki, oraz middleware przez zwykłe zawijanie funkcji. Czego wciąż nie otrzymasz: grup tras, łańcuchów middleware możliwych do złożenia, wiązania struktur (struct binding) ani typów parametrów per trasa.
Pretendenci
| net/http (stdlib) | chi v5.3.2 | Gin v1.12.0 | Echo v4.16.0 | Fiber v2.52.15 | |
|---|---|---|---|---|---|
| Silnik HTTP | net/http | net/http | net/http | net/http | fasthttp |
| Sygnatura handlera | func(w, r) |
func(w, r) |
func(c *gin.Context) |
func(c echo.Context) error |
func(c *fiber.Ctx) error |
| Kompatybilność z http.Handler | tak | tak | tak | tak | nie |
| Ekosystem middleware | rosnący, w stylu stdlib | w stylu stdlib | największy, specyficzny dla Gin | duży, specyficzny dla Echo | rosnący, specyficzny dla fasthttp |
| Wiązanie/walidacja | ręczne | ręczne | wbudowane tagi | wbudowane | przez pomocnicze |
| HTTP/2 | tak | tak | tak | tak | nie |
| Zależności | 0 | 1 | ~6 | ~8 | ~4 |
Wszystkie pięć stosuje routery w stylu trie lub radix z parametrami ścieżki i grupowanymi trasami. Podział strukturalny widać w ostatniej kolumnie sygnatur handlerów: chi i standardowa biblioteka mówią prostym http.Handler; Gin, Echo i Fiber zawijają żądania we własnym typie kontekstu. Fiber jest tu ponownie wyjątkiem, ponieważ opiera się na fasthttp, a nie net/http.
Co zbadałem benchmarkami
Zamiast cytować syntetyczne benchmarki routerów, uruchomiłem tę samą usługę na wszystkich pięciu stackach. Konfiguracja testowa:
- 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 wątków, localhost, bez TLS
- Trzy trasy per stack:
GET /health,GET /users/{id},GET /api/v1/users/{uid}/orders/{oid} - Obciążenie: 8 workerów z keep-alive przez 3 sekundy na trasie z dwoma parametrami, po przejściu rozgrzewkowym
- Każdy handler marszuje mały dokument JSON per żądanie za pomocą
encoding/json— więc liczby obejmują serializację, a nie tylko routing
Wersja standardowej biblioteki dla „gorącej” trasy:
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")))
})
oraz odpowiednik w Fibercie:
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")))
})
Dwie uwagi na temat uczciwości przed pokazaniem wyników. Po pierwsze, to mikrouciążenie na lokalnym hoście: brak bazy danych, brak negocjacji TLS, brak łańcucha middleware. Rzeczywiste usługi są wąskim miejscem w I/O — udział frameworku w żądaniu zapytującym PostgreSQL za pomocą GORM, Ent, Bun lub sqlc jest błędem zaokrąglenia. Po drugie, każdy stack miał własny proces serwera i identyczną strukturę handlera, więc porównanie dotyczy stacków, a nie ręcznie dostrojonych konfiguracji.
Wyniki
Przepustowość na dynamicznej trasie z dwoma parametrami:
| Stack | Żądania/sek | Śr. opóźnienie |
|---|---|---|
| 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 |
Cztery stacki oparte na net/http mieszczą się w granicach 4% różnicy. Silnik fasthttp w Fibercie wyprzedza je o około 21% w tym lokalnym obciążeniu — to realna różnica, ale typu, który staje się irrelewantny w produkcji przy jednym przebiegu bazy danych (0,5–5 ms).
Aby zobaczyć, dokąd faktycznie trafia koszt per żądanie, zbadałem też wywoływanie handlera bez sieci, wywołując każdy router bezpośrednio przez http.Handler.ServeHTTP (Fiber przez jego handler fasthttp):
| Stack | Czas/op | Bajtów/op | Alokacje/op |
|---|---|---|---|
| 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 |
* Wartość dla Fibera niesie narzut testowego szkieletu (harness overhead) — test musi kopiować żądanie do wielorazowego kontekstu fasthttp w każdej iteracji, czego rzeczywisty serwer unika, czytając bezpośrednio z przewodu. Traktuj to jako górne ograniczenie; sprawiedliwym porównaniem dla Fibera jest przepustowość na poziomie serwera przedstawiona powyżej.
Profil alokacji jest bardziej trwałym tematem. Gin i Echo poolują swoje konteksty żądań, więc żądanie przez nie kosztuje 3 alokacje. 5 alokacji w standardowej bibliotece przypada na samą infrastrukturę żądań o zakresie sesji. chi przechowuje parametry tras w żądaniowym context.Context, co objawia się jako 897 B i 7 alokacji per żądanie — to cena utrzymania czystego http.Handler z bogatymi informacjami o trasach.
Fiber i kompromis fasthttp
Fiber jest najszybszym stackiem w benchmarku serwerowym, a szybkość wiąże się z rachunkiem za kompatybilność. fasthttp to implementacja HTTP od zera z własnymi typami żądań/odpowiedzi, więc:
- Middleware z
net/httpnie doczepi się. Każda biblioteka oczekującahttp.Handlerlub*http.Requestpotrzebuje konwersji lub portu natywnego dla Fibera. - Brak HTTP/2. fasthttp go nie implementuje; Go
net/httpotrzymuje h2 za darmo z standardowej biblioteki przez TLS. - WebSocket, strumieniowanie i triki typu
http.Flusheridą ścieżką fasthttp, która różni się od zachowania stdlib, jakie zakłada większość dokumentacji Go.
Co Fiber kupuje w zamian: najszybsze liczby w powyższej tabeli, API w stylu Express i niższe alokacje, niż sugerują benchmarki frameworków, gdy uwzględnimy przewagę w parsowaniu przewodu. Pasuje to, gdy odpowiedzi są małe, routing dominuje, HTTP/2 jest kończony po stronie wstępnej (upstream), a potrzebowane middleware istnieje w ekosystemie Fibera. Te warunki spełniają niektóre usługi brzegowe (edge services) i prawie nic innego.
chi: nudna, środkowa droga
chi to router i pipeline middleware, nic więcej. Handlerzy to func(w, r), middleware to func(http.Handler) http.Handler, a wszystko napisane dla standardowej biblioteki doczepia się bez adapterów:
r := chi.NewRouter()
r.Use(RequestID, RealIP, Logger, Recoverer)
r.Route("/api/v1", func(r chi.Router) {
r.Get("/users/{uid}/orders/{oid}", getOrder)
})
To jest cała propozycja: grupy tras i stos middleware na wierzchu standardowego interfejsu. Tabela alokacji ocenia to uczciwie — bogatszy kontekst trasy per żądanie niż sama standardowa biblioteka, w zamian za składowalność middleware i zagnieżdżone trasy. Dla usług, które chcą struktury bez tożsamości frameworka, chi plus net/http to konfiguracja, na którą zbiega się większość zespołów Go.
Gin vs Echo, w kodzie
Gin i Echo to niemal bliźniaki pod względem wydajności — w granicach błędu w obu benchmarkach — więc wybór między nimi to kwestia gustu API i filozofii obsługi błędów.
Echo centralizuje błędy w HTTPErrorHandler. Handlerzy zwracają error, a jedna funkcja mapuje błędy na odpowiedzi:
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)
}
Handlerzy Gina nie zwracają nic; błędy gromadzą się na kontekście i renderujesz je tam, gdzie wybierzesz:
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)
}
Model Echo mapuje się czysto na architekturę obsługi błędów w Go z zawijanymi błędami i scentralizowaną obsługą granic — jedno miejsce tłumaczy wyniki errors.Is na kody statusu. Model Gina pasuje do bardziej powszechnego wzorca decydowania per handler. Poza błędami, decydujące przewagi Gina to rozmiar ekosystemu i wbudowane wiązanie struktur z tagami walidacji; przewagą Echo jest spójne API kontekstu i silniejsze wbudowane middleware. Oba integrują generowanie Swaggera przez własne adaptery — konfiguracja per framework jest omówiona w Dodawanie Swaggera do Twojego API w Go.
Jeszcze jeden oś: propagacja kontekstu. Gin i Echo zawijają context.Context wewnątrz swoich typów kontekstu, więc przekazanie anulowania do handlerów wymaga kroku ekstrakcji. Wzorce utrzymywania anulowania, limitów czasowych i wartości w ryzach przez którekolwiek z tych zawijania znajdują się w Go context.Context Dobrze.
Limity czasowe, których nikt nie ustawia
Każdy stack w tym porównaniu pozostawia limity czasowe serwera na wartościach domyślnych równych zero, jeśli ich nie ustawisz — a zero oznacza brak limitu. Klient, który otworzy połączenie i nie wyśle nic, utrzymuje gorutinę i deskryptora pliku w nieskończoność:
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 to ten, który należy ustawić w pierwszej kolejności — ogranicza wyczerpywanie połączeń w stylu slowloris, nie psując długich przesyłań tak, jak może to zrobić całkowite ReadTimeout. WriteTimeout musi pokrywać najwolniejszą uzasadnioną odpowiedź, co czyni go decyzją polityczną związaną z budżetem limitów czasowych, a nie stałą do skopiowania. Ta sama dyscyplina dotyczy per framework: Gin, Echo i Fiber akceptują konfigurację limitów czasowych na poziomie serwera, a żaden z nich nie włącza jej domyślnie.
Wybór
lub wymaganie zerowych zależności?} B -- tak --> C[net/http ServeMux] B -- nie --> D{Chcesz wbudowane wiązanie,
walidację, duży katalog middleware?} D -- nie --> E[chi + net/http] D -- tak --> F{Konwencja zespołu?} F -- handlerzy zwracające błędy --> G[Echo] F -- największy ekosystem, idiomatyka Gin --> H[Gin] A --> I{Małe odpowiedzi, ekstremalna przepustowość,
bez HTTP/2, kończenie TLS upstream?} I -- wszystko prawdą --> J[Fiber] I -- nie --> E
| Sytuacja | Wybór |
|---|---|
| Usługa wewnętrzna, endpoint CLI, plugin, biblioteka | sam net/http |
| Większość API — struktura bez lock-inu frameworka | chi na wierzchu net/http |
| Publiczne API, większy zespół, walidacja i wiązanie od razu | Gin |
| Publiczne API, scentralizowane tłumaczenie błędów, spójne API kontekstu | Echo |
| Ustalony wąski szyjek routingu/serializacji, mały JSON, bez HTTP/2 | Fiber |
Gdyby uczciwym domyślnym wyborem dla nowej usługi w 2026 roku musiała być nazwana konkretna ścieżka: zacznij od net/http, dodaj chi, gdy middleware lub grupy tras zaczną boleć, a przechodź do Gina lub Echo tylko wtedy, gdy wiązanie, walidacja lub konwencja zespołu uzasadnia tożsamość frameworka. Fiber pozostaje narzędziem specjalistycznym — warto go benchmarkować, gdy udowodniono, że przepustowość jest problemem, wyceniając cenę za kompatybilność, jaką pobiera.
Migracja między stackami
Koszt migracji śledzi podział sygnatur handlerów. stdlib ↔ chi jest mechaniczne: handlerzy zachowują tę samą sygnaturę, trasy przechodzą z wzorców mux.HandleFunc do r.Get, a middleware doczepia się do stosu chi. Gin ↔ Echo to tłumaczenie typu kontekstu — c.Param, c.JSON i logika middleware przenoszą się niemal linia po linii. Wszystko przenoszone do Fibera lub z niego to przepis warstwy HTTP, ponieważ kod oparty na *http.Request, middleware z net/http i założenia dotyczące h2 nie przetrwają granicy fasthttp; najłagodniejszą ścieżką jest trzymanie logiki biznesowej w pakietach bez frameworka i regenerowanie warstwy transportowej, co jest też strukturą, która czyni następną migrację tanią.