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 metodo | Contiene |
|---|---|
r.Method | GET, POST, ... |
r.URL.Path | il 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.Body | il 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.
| Pattern | Corrisponde 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.
w.Header().Set(...)modifica gli header. Ha effetto solo finché lo status non è stato inviato.w.WriteHeader(code)invia la riga di status e gli header.w.Write(...)(oppurefmt.Fprint(w, ...), o un encoder) invia il body. SeWriteHeadernon è stato chiamato, il primoWriteinvia prima200 OK.
Conseguenze:
- Impostare un header dopo che il body è partito non fa niente.
- Chiamare
WriteHeaderdue volte scrive nel loghttp: superfluous response.WriteHeader calle mantiene il primo status. - Dopo una risposta di errore, fai
return.http.Errornon 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 chego.moddicago 1.22o 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.