Un serveur en quelques lignes
Un handler est une fonction qui reçoit la requête et écrit la réponse. ServeMux achemine les requêtes vers les handlers.
L'éditeur ne peut pas accepter de connexions depuis votre navigateur, donc les exemples de cette page démarrent le serveur avec httptest.NewServer et l'appellent depuis le même programme. Dans un vrai programme, la dernière partie est remplacée par une seule ligne qui bloque et sert indéfiniment :
log.Fatal(http.ListenAndServe(":8080", mux))
Ensuite curl localhost:8080/hello/gopher affiche Hello, gopher!. ListenAndServe ne retourne qu'en cas d'erreur (le port est déjà pris, par exemple), c'est pourquoi il est enveloppé dans log.Fatal.
Chaque requête s'exécute dans sa propre goroutine. Tout ce que vos handlers partagent, comme une map ou un compteur, a besoin d'un mutex.
Handlers
Tout ce qui a une méthode ServeHTTP(http.ResponseWriter, *http.Request) est un http.Handler. http.HandlerFunc adapte une simple fonction à cette interface, et mux.HandleFunc fait la conversion pour vous. Un handler sous forme de struct est pratique quand les handlers ont besoin de dépendances :
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)
Le *http.Request vous donne :
| Champ ou méthode | Contient |
|---|---|
r.Method | GET, POST, ... |
r.URL.Path | le chemin, /items/42 |
r.PathValue("id") | un joker du motif de route (Go 1.22) |
r.URL.Query().Get("q") | un paramètre de la query string |
r.Header.Get("Authorization") | un en-tête de la requête |
r.Body | le body de la requête, un io.ReadCloser (le serveur le ferme) |
r.FormValue("name") | un champ de formulaire ou un paramètre de requête |
r.Context() | un context annulé quand le client se déconnecte |
Motifs de routage (Go 1.22)
Depuis Go 1.22, les motifs de ServeMux ont la forme [METHOD ][HOST]/[PATH], et les chemins peuvent contenir des jokers.
| Motif | Correspond à |
|---|---|
"/items/" | /items/ et tout ce qui est en dessous (barre oblique finale = préfixe) |
"/items" | seulement /items |
"GET /items/{id}" | GET (et HEAD) sur /items/42 ; r.PathValue("id") == "42" |
"POST /items" | seulement POST sur /items |
"/files/{path...}" | /files/a/b/c ; path vaut "a/b/c" |
"/{$}" | seulement /, pas tous les chemins |
"/" | tous les chemins qu'aucun autre motif ne capte |
Quand deux motifs correspondent, le plus spécifique l'emporte, donc /items/new bat /items/{id}. Si aucun n'est plus spécifique, par exemple /items/{id} et /{kind}/new (qui correspondent tous deux à /items/new), enregistrer le second provoque un panic avec un message qui nomme les deux motifs. Si un chemin correspond mais pas la méthode, le mux répond 405 Method Not Allowed avec un en-tête Allow, sans aucun code de votre part.
Le motif "/" est le fourre-tout. Cela surprend ceux qui enregistrent une page d'accueil sur "/" et la voient répondre 200 à toute URL inconnue. Utilisez "GET /{$}" pour la page d'accueil.
Ces motifs exigent go 1.22 ou plus dans go.mod. Avec une ligne de version plus ancienne, le mux revient à l'ancien comportement : il lit "GET /items" comme un nom d'hôte suivi d'un chemin, donc la route ne correspond jamais et chaque requête vers /items reçoit un 404.
Une petite API JSON
La dernière requête utilise DELETE, qu'aucune route n'accepte, et le mux répond 405 avec Allow: GET, HEAD tout seul : ce sont les méthodes enregistrées pour ce chemin (une route GET accepte aussi HEAD).
Codes de statut et ordre des écritures
Une réponse a trois parties, et elles doivent être écrites dans l'ordre : en-têtes, statut, body.
w.Header().Set(...)modifie les en-têtes. Cela n'a d'effet que tant que le statut n'est pas envoyé.w.WriteHeader(code)envoie la ligne de statut et les en-têtes.w.Write(...)(oufmt.Fprint(w, ...), ou un encodeur) envoie le body. SiWriteHeadern'a pas été appelé, le premierWriteenvoie d'abord200 OK.
Conséquences :
- Définir un en-tête après le début du body ne fait rien.
- Appeler
WriteHeaderdeux fois journalisehttp: superfluous response.WriteHeader callet garde le premier statut. - Après une réponse d'erreur, faites
return.http.Errorn'arrête pas votre handler, et le code qui suit continue d'écrire dans la même réponse.
Utilisez les constantes nommées (http.StatusOK, http.StatusCreated, http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError) plutôt que des nombres bruts. http.StatusText(404) renvoie "Not Found".
Middlewares
Un middleware est une fonction qui prend un handler et renvoie un handler, en faisant quelque chose avant ou après l'appel au suivant :
La ligne log: apparaît toujours avant client got: pour une même requête. La goroutine du handler l'affiche avant de retourner, et le serveur ne termine pas la réponse avant le retour du handler.
Le statusRecorder embarque le vrai ResponseWriter et ne redéfinit que WriteHeader, ce qui est la façon standard d'observer le code de statut depuis un middleware.
Réglages de production
http.ListenAndServe utilise un serveur sans timeouts, donc un client lent peut garder une connexion ouverte indéfiniment. Configurez explicitement un http.Server pour tout ce qui est exposé à Internet, et arrêtez-le proprement :
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 cesse d'accepter de nouvelles connexions et attend la fin des requêtes actives, jusqu'à l'échéance du context. ListenAndServe renvoie http.ErrServerClosed dès que Shutdown commence, c'est pourquoi cette erreur n'est pas traitée comme un échec. Pour les handlers de longue durée, passez r.Context() plus bas pour qu'ils s'arrêtent quand le client s'en va.
Servir des fichiers statiques tient en une ligne : mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).
Erreurs courantes
- Ne pas retourner après
http.Error. Le handler continue de s'exécuter et écrit davantage dans la réponse. - Définir des en-têtes après avoir écrit le body. Ils sont ignorés sans bruit.
- Enregistrer la page d'accueil sur
"/". Elle devient le fourre-tout pour tous les chemins inconnus. Utilisez"/{$}". - Partager un état entre handlers sans verrou. Les requêtes s'exécutent en concurrence.
- Utiliser le serveur par défaut en production. Il n'a pas de timeouts ; définissez-les sur un
http.Server. - Des motifs de routage ignorés. Si
"GET /x/{id}"ne correspond jamais, vérifiez quego.modindiquego 1.22ou plus.
Questions fréquentes
Comment créer un serveur web simple en Go ?
Enregistrez un handler et commencez à écouter : http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) puis log.Fatal(http.ListenAndServe(":8080", nil)). Le serveur de la bibliothèque standard est prêt pour la production ; il gère HTTP/1.1, HTTP/2 sur TLS, le keep-alive et une goroutine par connexion.
Comment récupérer un paramètre de chemin avec net/http en Go ?
Depuis Go 1.22, les motifs de ServeMux peuvent contenir des jokers : enregistrez mux.HandleFunc("GET /items/{id}", h) et lisez la valeur dans le handler avec r.PathValue("id"). Un {path...} final correspond au reste du chemin. Avant 1.22, il fallait découper r.URL.Path vous-même ou utiliser un routeur comme chi.
Faut-il un framework comme Gin pour construire une API REST en Go ?
Non. Depuis Go 1.22, net/http route par méthode et par paramètres de chemin, ce qui couvrait la principale raison d'utiliser un routeur. Avec encoding/json pour les bodies et de petites fonctions middleware pour les logs et l'authentification, la bibliothèque standard suffit pour la plupart des API. Les frameworks ajoutent des commodités comme le binding et la validation des requêtes.
Comment définir le code de statut dans un handler HTTP Go ?
Appelez w.WriteHeader(http.StatusCreated) avant d'écrire le body. Si vous écrivez d'abord le body, Go envoie automatiquement 200 OK et un WriteHeader ultérieur est ignoré avec un message de log superfluous response.WriteHeader call. Définissez aussi les en-têtes avec w.Header().Set avant WriteHeader ; pour les erreurs, http.Error(w, msg, code) fait tout cela.