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.

Page content

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.

five 3D glass blocks connected by glowing lines, one per Go HTTP stack

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/http nie doczepi się. Każda biblioteka oczekująca http.Handler lub *http.Request potrzebuje konwersji lub portu natywnego dla Fibera.
  • Brak HTTP/2. fasthttp go nie implementuje; Go net/http otrzymuje h2 za darmo z standardowej biblioteki przez TLS.
  • WebSocket, strumieniowanie i triki typu http.Flusher idą ś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

flowchart TD A[Nowa usługa HTTP w Go] --> B{Narzędzie wewnętrzne, plugin,
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ą.

Przydatne linki

Subskrybuj

Otrzymuj nowe wpisy o systemach, infrastrukturze i inżynierii AI.