Menu

Server HTTP in Golang: routing net/http, JSON e status code

Come costruire un web server con il package standard net/http di Go: handler, routing con ServeMux per metodo e wildcard nel percorso (Go 1.22), risposte JSON, status code, middleware, timeout e shutdown graduale.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un server in poche righe

Un handler è una funzione che riceve la richiesta e scrive la risposta. ServeMux instrada le richieste verso gli handler.

L'editor non può accettare connessioni dal tuo browser, quindi gli esempi di questa pagina avviano il server con httptest.NewServer e lo chiamano dallo stesso programma. In un programma reale l'ultima parte è sostituita da una riga che si blocca e serve richieste per sempre:

log.Fatal(http.ListenAndServe(":8080", mux))

A quel punto curl localhost:8080/hello/gopher stampa Hello, gopher!. ListenAndServe ritorna solo in caso di errore (la porta è già occupata, per esempio), ed è per questo che è avvolto in log.Fatal.

Ogni richiesta gira nella propria goroutine. Tutto ciò che i tuoi handler condividono, come una map o un contatore, ha bisogno di un mutex.

Handler

Qualsiasi cosa abbia un metodo ServeHTTP(http.ResponseWriter, *http.Request) è un http.Handler. http.HandlerFunc adatta una funzione normale a quell'interfaccia, e mux.HandleFunc fa la conversione per te. Un handler basato su struct è comodo quando gli handler hanno delle dipendenze:

type API struct {
	db *sql.DB
}

func (a *API) listItems(w http.ResponseWriter, r *http.Request) { /* uses a.db */ }

mux.HandleFunc("GET /items", api.listItems)

L'*http.Request ti dà:

Campo o metodoContiene
r.MethodGET, POST, ...
r.URL.Pathil percorso, /items/42
r.PathValue("id")una wildcard del pattern della route (Go 1.22)
r.URL.Query().Get("q")un parametro della query string
r.Header.Get("Authorization")un header della richiesta
r.Bodyil body della richiesta, un io.ReadCloser (lo chiude il server)
r.FormValue("name")un campo di un form o un parametro di query
r.Context()un context cancellato quando il client si disconnette

Pattern di routing (Go 1.22)

Da Go 1.22, i pattern di ServeMux hanno la forma [METHOD ][HOST]/[PATH], e i percorsi possono contenere wildcard.

PatternCorrisponde a
"/items/"/items/ e tutto ciò che sta sotto (slash finale = prefisso)
"/items"solo /items
"GET /items/{id}"GET (e HEAD) su /items/42; r.PathValue("id") == "42"
"POST /items"solo POST su /items
"/files/{path...}"/files/a/b/c; path è "a/b/c"
"/{$}"solo /, non ogni percorso
"/"ogni percorso che nessun altro pattern cattura

Quando corrispondono due pattern, vince quello più specifico, quindi /items/new batte /items/{id}. Se nessuno dei due è più specifico, per esempio /items/{id} e /{kind}/new (entrambi corrispondono a /items/new), registrare il secondo causa un panic con un messaggio che nomina entrambi i pattern. Se un percorso corrisponde ma il metodo no, il mux risponde 405 Method Not Allowed con un header Allow, senza bisogno di codice da parte tua.

Il pattern "/" è quello pigliatutto. Sorprende chi registra una home page con "/" e la trova a rispondere 200 a ogni URL sconosciuto. Usa "GET /{$}" per la home page.

Questi pattern richiedono go 1.22 o successivo in go.mod. Con una riga di versione più vecchia, il mux torna al comportamento precedente: legge "GET /items" come un nome host seguito da un percorso, quindi la route non corrisponde mai e ogni richiesta a /items riceve un 404.

Una piccola API JSON

L'ultima richiesta usa DELETE, che nessuna route accetta, e il mux risponde da solo 405 con Allow: GET, HEAD: sono i metodi registrati per quel percorso (una route GET accetta anche HEAD).

Status code e ordine delle scritture

Una risposta ha tre parti, e vanno scritte in ordine: header, status, body.

  1. w.Header().Set(...) modifica gli header. Ha effetto solo finché lo status non è stato inviato.
  2. w.WriteHeader(code) invia la riga di status e gli header.
  3. w.Write(...) (oppure fmt.Fprint(w, ...), o un encoder) invia il body. Se WriteHeader non è stato chiamato, il primo Write invia prima 200 OK.

Conseguenze:

  • Impostare un header dopo che il body è partito non fa niente.
  • Chiamare WriteHeader due volte scrive nel log http: superfluous response.WriteHeader call e mantiene il primo status.
  • Dopo una risposta di errore, fai return. http.Error non ferma il tuo handler, e il codice che viene dopo continua a scrivere nella stessa risposta.

Usa le costanti con nome (http.StatusOK, http.StatusCreated, http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError) invece dei numeri nudi. http.StatusText(404) restituisce "Not Found".

Middleware

Un middleware è una funzione che prende un handler e restituisce un handler, facendo qualcosa prima o dopo aver chiamato il successivo:

Per la stessa richiesta, la riga log: compare sempre prima di client got:. La goroutine dell'handler la stampa prima di ritornare, e il server non completa la risposta finché l'handler non ritorna.

Lo statusRecorder incorpora il vero ResponseWriter e sovrascrive solo WriteHeader, che è il modo standard per osservare lo status code da un middleware.

Impostazioni per la produzione

http.ListenAndServe usa un server senza timeout, quindi un client lento può tenere aperta una connessione a tempo indeterminato. Configura esplicitamente un http.Server per qualsiasi cosa esposta su internet, e spegnilo in modo graduale:

srv := &http.Server{
	Addr:              ":8080",
	Handler:           mux,
	ReadHeaderTimeout: 5 * time.Second,
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      30 * time.Second,
	IdleTimeout:       120 * time.Second,
}

go func() {
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}()

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // wait for Ctrl+C or a SIGTERM from the orchestrator

shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // finish in-flight requests
	log.Println("shutdown:", err)
}

Shutdown smette di accettare nuove connessioni e aspetta che le richieste attive finiscano, fino alla scadenza del context. ListenAndServe restituisce http.ErrServerClosed non appena parte Shutdown, ed è per questo che quell'errore non viene trattato come un fallimento. Per gli handler che durano a lungo, passa r.Context() verso il basso così si fermano quando il client se ne va.

Servire file statici richiede una riga: mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).

Errori comuni

  • Non fare return dopo http.Error. L'handler continua a girare e scrive altro nella risposta.
  • Impostare header dopo aver scritto il body. Vengono scartati senza avvisi.
  • Registrare la home page su "/". Diventa il pigliatutto per ogni percorso sconosciuto. Usa "/{$}".
  • Condividere stato tra handler senza lock. Le richieste girano in modo concorrente.
  • Usare il server di default in produzione. Non ha timeout; impostali su un http.Server.
  • Pattern di routing ignorati. Se "GET /x/{id}" non corrisponde mai, controlla che go.mod dica go 1.22 o successivo.

Domande frequenti

Come creo un semplice web server in Go?

Registra un handler e mettiti in ascolto: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) e poi log.Fatal(http.ListenAndServe(":8080", nil)). Il server della libreria standard è pronto per la produzione; gestisce HTTP/1.1, HTTP/2 su TLS, keep-alive e una goroutine per connessione.

Come ottengo un parametro del percorso con net/http in Go?

Da Go 1.22, i pattern di ServeMux possono contenere wildcard: registra mux.HandleFunc("GET /items/{id}", h) e leggi il valore dentro l'handler con r.PathValue("id"). Un {path...} finale corrisponde al resto del percorso. Prima della 1.22 dovevi dividere r.URL.Path a mano o usare un router come chi.

Mi serve un framework come Gin per costruire una API REST in Go?

No. Da Go 1.22, net/http instrada per metodo e parametri del percorso, che era il motivo principale per cui si usavano i router. Con encoding/json per i body e piccole funzioni middleware per logging e autenticazione, la libreria standard basta per la maggior parte delle API. I framework aggiungono comodità come il binding delle richieste e la validazione.

Come imposto lo status code in un handler HTTP di Go?

Chiama w.WriteHeader(http.StatusCreated) prima di scrivere il body. Se scrivi prima il body, Go invia 200 OK in automatico e un WriteHeader successivo viene ignorato con il messaggio di log superfluous response.WriteHeader call. Imposta anche gli header con w.Header().Set prima di WriteHeader; per gli errori, http.Error(w, msg, code) fa tutto questo da solo.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA