Web Framework Go nel 2026: confronto tra net/http, chi, Gin, Echo e Fiber

Cinque stack Go HTTP, un unico banco di prova, numeri reali.

Indice

Go include un server HTTP di livello production nella sua libreria standard, e dal Go 1.22 il router integrato gestisce da solo l’abbinamento dei metodi e i parametri del percorso. La questione relativa ai framework non è scomparsa, ma si è ristretta.

Cinque stack coprono quasi ogni decisione di backend in Go nel 2026: net/http della libreria standard, chi, Gin, Echo e Fiber. Ognuno occupa una posizione diversa sullo stesso compromesso — ciò che la libreria ti offre rispetto a ciò che le devi in termini di dipendenze, convenzioni e compatibilità con l’ecosistema. Questo articolo li confronta in base alla potenza di routing, all’ergonomia dei handler, ai middleware e alle prestazioni misurate, per poi concludere con una tabella di decisione anziché con un unico verdetto.

Se stai partendo da zero e desideri il processo di costruzione completo attorno alla tua scelta — validazione, autenticazione, test, deployment — la versione passo-passo si trova in Costruire API REST in Go. Qui l’attenzione resta concentrata sugli stack in sé.

Cinque blocchi di vetro 3D collegati da linee luminose, uno per ogni stack HTTP di Go

Il reset di ServeMux post-1.22

Il motivo per cui il dibattito sui framework ha cambiato forma è una sola aggiunta alla libreria standard. Go 1.22 ha dato a http.ServeMux pattern consapevoli del metodo e parametri di percorso:

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // ...
})
mux.HandleFunc("POST /users", createUser)

Prima di questo, qualsiasi API che avesse bisogno di GET /users/{id} richiedeva un router di terze parti, ed è così che Gin, chi ed Echo sono diventati lo standard di default. Nel 2026, la libreria standard copre rotte statiche, corrispondenza dei metodi, parametri a segmento singolo e wildcard, e middleware tramite semplice avvolgimento di funzioni. Ciò che ancora non ti offre: gruppi di rotte, catene di middleware componibili, binding strutturato o tipi di parametri per singola rotta.

I candidati

net/http (stdlib) chi v5.3.2 Gin v1.12.0 Echo v4.16.0 Fiber v2.52.15
Motore HTTP net/http net/http net/http net/http fasthttp
Firma del handler func(w, r) func(w, r) func(c *gin.Context) func(c echo.Context) error func(c *fiber.Ctx) error
Compatibile con http.Handler sì sì sì sì no
Ecosistema middleware in crescita, forma stdlib forma stdlib il più grande, specifico di Gin ampio, specifico di Echo in crescita, specifico di fasthttp
Binding/validazione manuale manuale tag integrati integrato tramite helper
HTTP/2 sì sì sì sì no
Dipendenze 0 1 ~6 ~8 ~4

Tutti e cinque utilizzano router di tipo trie o radix con parametri di percorso e rotte raggruppate. La divisione strutturale è l’ultima riga della storia della firma dei handler: chi e la libreria standard parlano di semplice http.Handler; Gin, Echo e Fiber avvolgono le richieste nel loro tipo di contesto. Fiber è l’eccezione ancora una volta, perché si basa su fasthttp, non su net/http.

Cosa ho misurato

Invece di citare benchmark di router sintetici, ho eseguito lo stesso servizio su tutti e cinque gli stack. La configurazione:

  • 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 thread, localhost, senza TLS
  • Tre rotte per stack: GET /health, GET /users/{id}, GET /api/v1/users/{uid}/orders/{oid}
  • Carico: 8 worker keep-alive per 3 secondi contro la rotta a due parametri, dopo un passaggio di riscaldamento
  • Ogni handler serializza un piccolo documento JSON per richiesta con encoding/json — quindi i numeri includono la serializzazione, non solo il routing

La versione della libreria standard della rotta “calda”:

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")))
})

e l’equivalente in Fiber:

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")))
})

Due note di onestà prima dei numeri. Prima, questo è un micro-carico di lavoro su localhost: nessun database, nessuna handshake TLS, nessuna catena di middleware. I servizi reali hanno collo di bottiglia sull’I/O — la quota del framework in una richiesta che interroga PostgreSQL tramite GORM, Ent, Bun, o sqlc è un errore di arrotondamento. Seconda, ogni stack ha avuto il proprio processo di server e una forma di handler identica, quindi il confronto è tra stack, non tra configurazioni ottimizzate a mano.

Risultati

Throughput sulla rotta dinamica a due parametri:

Stack Richieste/secondo Latenza media
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

I quattro stack basati su net/http si attestano entro il 4% l’uno dall’altro. Il motore fasthttp di Fiber è in vantaggio di circa il 21% su questo carico di lavoro localhost — un dato reale, ma del tipo che un singolo roundtrip al database (0,5–5 ms) rende irrilevante in produzione.

Per vedere dove va effettivamente il costo per richiesta, ho eseguito anche benchmark dell’invocazione dei handler senza la rete, chiamando ogni router direttamente tramite http.Handler.ServeHTTP (Fiber tramite il suo handler fasthttp):

Stack Tempo/op Byte/op Allocazioni/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

* Il valore di Fiber include l’overhead dell’harness — il test deve copiare la richiesta nel contesto riutilizzabile di fasthttp a ogni iterazione, cosa che il server reale evita leggendo dal wire. Trattalo come un limite superiore; il throughput a livello di server sopra è il confronto equo per Fiber.

Il profilo delle allocazioni è la storia più duratura. Gin ed Echo mettono in pool i contesti delle richieste, quindi una richiesta attraverso di essi costa 3 allocazioni. Le 5 allocazioni della libreria standard vanno alla stessa gestione legata alla richiesta. chi memorizza i parametri della rotta nel context.Context della richiesta, cosa che emerge come 897 B e 7 allocazioni per richiesta — il prezzo per rimanere un semplice http.Handler con un’informazione di rotta ricca.

Fiber e il compromesso di fasthttp

Fiber è lo stack più veloce nel benchmark del server, e la velocità arriva con una bolletta di compatibilità. fasthttp è un’implementazione HTTP da zero con i propri tipi di richiesta/risposta, quindi:

  • I middleware net/http non si attaccano. Qualsiasi libreria che si aspetti un http.Handler o un *http.Request necessita di una conversione o di una porta nativa per Fiber.
  • Nessun HTTP/2. fasthttp non lo implementa; net/http di Go ottiene h2 dalla libreria standard gratuitamente su TLS.
  • WebSocket, streaming e trucchi in stile http.Flusher seguono tutti il percorso fasthttp, che differisce dal comportamento della libreria standard su cui la maggior parte della documentazione Go si basa.

Ciò che Fiber compra in cambio: i numeri più veloci nella tabella sopra, un’API simile a Express e allocazioni inferiori rispetto a quanto suggeriscano i benchmark dei framework una volta incluso il vantaggio di parsing del wire. Si adatta quando le risposte sono piccole, il routing domina, l’HTTP/2 viene terminato a monte e i middleware di cui hai bisogno esistono nell’ecosistema di Fiber. Queste condizioni valgono per alcuni servizi edge e per quasi nessun’altra cosa.

chi: il percorso centrale noioso

chi è un router e un pipeline di middleware, nient’altro. Gli handler sono func(w, r), i middleware sono func(http.Handler) http.Handler, e tutto ciò che è stato scritto per la libreria standard si attacca senza adattatori:

r := chi.NewRouter()
r.Use(RequestID, RealIP, Logger, Recoverer)
r.Route("/api/v1", func(r chi.Router) {
    r.Get("/users/{uid}/orders/{oid}", getOrder)
})

Questa è l’intera proposta: gruppi di rotte e un stack di middleware sopra l’interfaccia standard. La tabella delle allocazioni lo prezza onestamente — un contesto di rotta più ricco per richiesta rispetto alla sola libreria standard, in cambio di middleware componibili e rotte annidate. Per i servizi che vogliono struttura senza un’identità di framework, chi più net/http è la configurazione sulla quale la maggior parte dei team Go converge.

Gin vs Echo, nel codice

Gin ed Echo sono gemelli quasi identici nelle prestazioni — entro il rumore in entrambi i benchmark — quindi la scelta tra loro è una questione di gusto nell’API e di filosofia di gestione degli errori.

Echo centralizza gli errori in un HTTPErrorHandler. Gli handler restituiscono error, e una funzione mappa gli errori alle risposte:

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)
}

Gli handler di Gin non restituiscono nulla; gli errori si accumulano sul contesto e li rendi dove scegli tu:

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)
}

Il modello di Echo si mappa pulitamente sull’architettura di gestione degli errori in Go con errori avvolti e gestione centralizzata del confine — un luogo unico traduce i risultati di errors.Is in codici di stato. Il modello di Gin corrisponde al modello più comune di decidere per ogni handler. Oltre agli errori, i vantaggi decisivi di Gin sono la dimensione dell’ecosistema e il binding strutturato integrato con tag di validazione; quelli di Echo sono un’API di contesto coerente e middleware integrati più robusti. Entrambi integrano la generazione di Swagger tramite i propri adattatori — la configurazione specifica di ogni framework è coperta in Aggiungere Swagger alla tua API Go.

Un asse in più: propagazione del contesto. Gin ed Echo avvolgono context.Context all’interno dei propri tipi di contesto, quindi passare l’annullamento agli handler richiede un passaggio di estrazione. I modelli per mantenere in ordine annullamento, timeout e valori attraverso uno qualsiasi di questi avvolgitori si trovano in Go context.Context fatto bene.

I timeout che nessuno imposta

Ogni stack in questo confronto lascia i timeout del server ai default zero se non li imposti — e zero significa nessun limite. Un client che apre una connessione e non invia nulla tiene occupati un goroutine e un file descriptor all’infinito:

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 è quello da impostare per primo — pone un limite all’esaurimento delle connessioni in stile slowloris senza spezzare i lunghi upload come può fare un generico ReadTimeout. WriteTimeout deve coprire la risposta legittima più lenta, il che lo rende una decisione di politica legata ai tuoi budget di timeout anziché una costante da copiare-incollare. La stessa disciplina si applica per framework: Gin, Echo e Fiber accettano tutti la configurazione dei timeout a livello di server, e nessuno li abilita di default.

Scelta

flowchart TD A[Nuovo servizio HTTP Go] --> B{Strumento interno, plugin,
o requisito di zero dipendenze?} B -- sì --> C[net/http ServeMux] B -- no --> D{Vuoi binding integrato,
validazione, grande catalogo middleware?} D -- no --> E[chi + net/http] D -- sì --> F{Convenzione del team?} F -- handler che restituiscono errori --> G[Echo] F -- ecosistema più grande, idiom Gin --> H[Gin] A --> I{Risposte piccole, throughput estremo,
no HTTP/2, terminazione TLS a monte?} I -- tutto vero --> J[Fiber] I -- no --> E
Situazione Scelta
Servizio interno, endpoint CLI, plugin, libreria solo net/http
La maggior parte delle API — struttura senza lock-in di framework chi sopra net/http
API pubblica, team più grande, validazione e binding fuori dalla scatola Gin
API pubblica, traduzione centralizzata degli errori, API di contesto coerente Echo
Collo di bottiglia provato in routing/serializzazione, JSON minuscolo, no HTTP/2 Fiber

Se il default onesto per un nuovo servizio nel 2026 deve essere nominato: parti con net/http, aggiungi chi quando middleware o gruppi di rotte iniziano a dare fastidio, e passa a Gin o Echo solo quando binding, validazione o convenzione del team giustificano un’identità di framework. Fiber resta uno strumento specialista — vale la pena di eseguirne il benchmark quando il throughput si dimostra essere il problema, valutandolo rispetto alla bolletta di compatibilità che impone.

Migrazione tra stack

Il costo della migrazione segue la divisione della firma dei handler. stdlib ↔ chi è meccanico: gli handler mantengono la stessa firma, le rotte si spostano dai pattern mux.HandleFunc a r.Get, i middleware si attaccano allo stack chi. Gin ↔ Echo è una traduzione di tipo contesto — c.Param, c.JSON e la logica dei middleware si portano quasi riga per riga. Qualsiasi cosa che si sposti verso o da Fiber è una riscrittura dello strato HTTP, perché il codice basato su *http.Request, i middleware di net/http e le ipotesi su h2 non sopravvivono al confine di fasthttp; il percorso meno doloroso è mantenere la logica di business in pacchetti liberi da framework e rigenerare lo strato di trasporto, il che è anche la struttura che rende la prossima migrazione economica.

Iscriviti

Ricevi nuovi articoli su sistemi, infrastruttura e ingegneria AI.