Perché il tuo HTTP server in Go ha bisogno di un sistema di routing e middleware?
Hai iniziato con http.HandleFunc e tutto funzionava. Poi sono arrivati gli endpoint con parametri, l'autenticazione, il logging, la gestione degli errori centralizzata. Il codice si è trasformato in una sequenza di if-else, switch-case e codice duplicato in ogni handler. Il problema non è Go: è che stai usando lo strumento giusto nel modo sbagliato.
Noi di Meteora Web lavoriamo con Go da anni per backend di piattaforme proprietarie. Siamo partiti anche noi dal semplice — poi abbiamo capito che un server HTTP ben strutturato è la differenza tra un progetto che si mantiene e uno che diventa un costo ogni volta che devi aggiungere una rotta.
Questa guida ti mostra come costruire un sistema di routing e middleware che scala, usando esclusivamente la libreria standard di Go (net/http). Niente framework esterni, niente dipendenze inutili. Solo codice che capisci e controlli.
Come funziona il multiplexer net/http in Go?
Il cuore di ogni server HTTP in Go è il http.ServeMux, un multiplexer che associa pattern URL a handler. Fino a Go 1.21, i pattern erano fissi: "/api/users" oppure "/api/" per prefissi. Niente parametri dinamici come :id.
Con Go 1.22 (e versioni successive), il multiplexer supporta pattern con parametri e metodi HTTP. Questo cambia tutto: puoi definire rotte come GET /users/{id} direttamente con la libreria standard, senza pacchetti esterni.
Sponsored Protocol
Esempio base: ServeMux con pattern moderni
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
// Pattern con metodo e parametro dinamico
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "User ID: %s", id)
})
mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "User creato")
})
log.Fatal(http.ListenAndServe(":8080", mux))
}Attenzione: r.PathValue() è disponibile da Go 1.22. Se lavori con versioni precedenti, devi usare pacchetti come gorilla/mux o chi — ma il consiglio è aggiornare Go e sfruttare la libreria standard.
Come si implementa il routing avanzato con pattern matching in Go 1.22+?
Il nuovo ServeMux supporta:
- Metodi HTTP espliciti:
GET /risorsa,POST /risorsa,PUT /risorsa/{id}. - Parametri dinamici:
{id},{slug},{path*}per match su più segmenti. - Priorità basata su specificità: pattern più specifici vincono su quelli più generici.
Esempio: routing con più metodi e parametri
mux.HandleFunc("GET /posts", listPosts)
mux.HandleFunc("POST /posts", createPost)
mux.HandleFunc("GET /posts/{slug}", getPost)
mux.HandleFunc("PUT /posts/{slug}", updatePost)
mux.HandleFunc("DELETE /posts/{slug}", deletePost)Nota: non serve più r.Method manuale. Il mux stesso reindirizza gli errori con 405 Method Not Allowed se il metodo non corrisponde.
Sponsored Protocol
Gestione degli asterisco wildcard
Usa {path*} per catturare il resto del percorso:
mux.HandleFunc("GET /files/{path*}", func(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path")
fmt.Fprintf(w, "Path richiesto: %s", path)
})Come creare middleware componibili per autenticazione, logging e recovery?
Un middleware in Go è una funzione che prende un http.Handler e restituisce un altro http.Handler. La firma standard è:
type Middleware func(http.Handler) http.HandlerPuoi comporre middleware come funzioni annidate. Vediamo i tre più utili per un backend serio.
Middleware di logging
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next.ServeHTTP(w, r)
})
}Middleware di autenticazione (API Key)
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
if key != "segreto-super-sicuro" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}Middleware di recovery (panic catching)
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}Comporre i middleware
Usa una funzione helper per applicare più middleware in sequenza:
Sponsored Protocol
func chainMiddleware(handler http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
handler = middlewares[i](handler)
}
return handler
}E poi applichi al mux:
mux := http.NewServeMux()
mux.Handle("GET /protected", chainMiddleware(
http.HandlerFunc(protectedHandler),
recoveryMiddleware,
authMiddleware,
loggingMiddleware,
))Oppure, se vuoi applicare middleware globalmente a tutte le rotte, usa un http.Handler wrapper:
Sponsored Protocol
finalHandler := chainMiddleware(mux, recoveryMiddleware, loggingMiddleware)
log.Fatal(http.ListenAndServe(":8080", finalHandler))Come gestire contesti e pattern decorator per handler complessi?
Spesso hai bisogno di passare dati (utente loggato, connessione DB) da un middleware all'handler. In Go si usa il context.Context tramite r.Context().
Middleware che arricchisce il contesto
func contextMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// esempio: utente estratto dal token
user := &User{ID: 42, Name: "Mario"}
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}Nell'handler recuperi il valore:
func protectedHandler(w http.ResponseWriter, r *http.Request) {
user := r.Context().Value("user").(*User)
fmt.Fprintf(w, "Benvenuto %s", user.Name)
}Attenzione: il contesto non va usato come discarica per qualsiasi dato. Usalo solo per valori legati al ciclo di vita della richiesta (autenticazione, trace ID, ecc.).
Quali errori evitare quando si costruisce un HTTP server in Go con net/http?
Ne abbiamo visti parecchi nei progetti che ci arrivano. Ecco i più comuni:
- Non gestire il body chiuso: usa
defer r.Body.Close()in ogni handler che legge il body. - Timeout assenti: un server senza timeout muore. Imposta
http.Server{ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second}. - Middleware globale senza esclusioni: a volte devi bypassare l'autenticazione su rotte di health check. Crea un percorso esplicito o usa pattern di esclusione.
- Ignorare gli errori di ListenAndServe:
http.ListenAndServetorna un errore se il server crasha. Loggalo o lancia panic in fase di sviluppo.
Cosa fare adesso
- Aggiorna Go alla versione 1.22+ se non lo hai già fatto. Il nuovo mux è un game changer.
- Riscrivi le tue rotte usando pattern con metodo e parametri. Elimina tutto lo switch su
r.Method. - Crea un file middleware.go con i middleware di logging, recovery e autenticazione. Sperimenta la composizione.
- Testa con httptest — la libreria standard ha
httptest.NewServerper testare i tuoi handler senza alzare un server reale. - Se vuoi approfondire l'ecosistema Go per backend, leggi la nostra guida principale su Go per Backend: Concorrenza, API REST e Microservizi.
Un server HTTP ben costruito con net/http ti dà il controllo totale. Nessuna dipendenza, nessuna magia nera. Solo codice che fa esattamente quello che hai scritto. Come piace a noi.