Menu

Serveur HTTP en Golang : routage net/http, JSON et codes de statut

Comment construire un serveur web avec le package standard net/http de Go : handlers, routage ServeMux avec méthodes et jokers de chemin (Go 1.22), réponses JSON, codes de statut, middlewares, timeouts et arrêt propre.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

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éthodeContient
r.MethodGET, POST, ...
r.URL.Pathle 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.Bodyle 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.

MotifCorrespond à
"/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.

  1. w.Header().Set(...) modifie les en-têtes. Cela n'a d'effet que tant que le statut n'est pas envoyé.
  2. w.WriteHeader(code) envoie la ligne de statut et les en-têtes.
  3. w.Write(...) (ou fmt.Fprint(w, ...), ou un encodeur) envoie le body. Si WriteHeader n'a pas été appelé, le premier Write envoie d'abord 200 OK.

Conséquences :

  • Définir un en-tête après le début du body ne fait rien.
  • Appeler WriteHeader deux fois journalise http: superfluous response.WriteHeader call et garde le premier statut.
  • Après une réponse d'erreur, faites return. http.Error n'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 que go.mod indique go 1.22 ou 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.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER