backend / go
net/http et API REST
Explication
Ce que vous allez apprendre
- Construire une API REST complète avec la seule bibliothèque standard, sans framework
- Router des requêtes par méthode et chemin avec
http.ServeMux(Go 1.22+) - Décoder et encoder du JSON dans les handlers HTTP
- Protéger un état partagé accédé concurremment par plusieurs requêtes
- Écrire un middleware pour ajouter un comportement transverse (logging)
Dans quel contexte ?
Une petite équipe doit livrer rapidement une API de gestion de tâches, sans vouloir intégrer un framework tiers pour un projet aussi simple. Go permet exactement ça : construire une API REST fonctionnelle, robuste et performante avec uniquement net/http, sans dépendance externe — une possibilité rare parmi les langages backend modernes.
D'abord, un rappel important sur le contexte historique
Pendant longtemps, router des requêtes par méthode HTTP et paramètres de chemin (GET /taches/{id}) nécessitait un framework tiers comme Gin ou Chi, car http.ServeMux ne savait distinguer que des chemins simples. Depuis Go 1.22 (2024), ServeMux gère nativement les méthodes et les paramètres de chemin, rendant un framework externe optionnel pour bien des projets.
Une fois le routage posé, il faut structurer les réponses proprement
Chaque handler doit définir le Content-Type, le code de statut HTTP, puis sérialiser la réponse en JSON. Centraliser cette logique dans des fonctions utilitaires (repondreJSON, repondreErreur) évite de dupliquer ce boilerplate dans chaque handler.
| Étape d'un handler REST | Fonction typique | Piège si oubliée |
|---|---|---|
| Décoder le corps de la requête | json.NewDecoder(r.Body).Decode(&payload) | Payload invalide non détecté |
| Valider les données | Vérification manuelle des champs requis | Données incohérentes en base |
| Répondre avec le bon code HTTP | http.StatusCreated, http.StatusNotFound... | Client qui interprète mal la réponse |
| Protéger l'état partagé | sync.RWMutex sur le stockage | Race condition sous forte charge |
Prérequis
Cette leçon combine des notions déjà vues : structs et tags (via JSON, détaillé dans la prochaine leçon), sync.RWMutex (leçon sur sync), et gestion d'erreurs idiomatique.
Il reste un piège de conception fréquent chez les débutants sur les API Go
Oublier de protéger la structure Stockage (qui contient la map des tâches) avec un mutex fonctionne parfaitement en test avec une seule requête à la fois, puis provoque des race conditions dès que plusieurs clients frappent l'API simultanément — un défaut typique invisible tant que la charge reste faible.
Piège fréquent
Toujours vérifier, avant de considérer un handler HTTP "terminé", s'il accède à un état partagé entre requêtes (une map en mémoire, un compteur...). Si oui, ce champ doit être protégé par un sync.RWMutex, comme démontré dans la leçon précédente sur sync.
Enfin, un middleware n'est qu'une fonction qui enveloppe un handler
func middlewareLog(next http.Handler) http.Handler prend un handler existant et en retourne un nouveau qui ajoute un comportement (ici, journaliser chaque requête) avant de déléguer au handler d'origine. C'est un pattern simple mais extrêmement puissant, à la base de toute l'authentification, du logging et du rate limiting en Go.
Bonne pratique
Toujours valider explicitement le contenu d'un payload JSON reçu (champs requis, types cohérents) avant de l'utiliser : ne jamais faire confiance implicitement à ce qu'envoie un client, même interne à l'entreprise.
Maintenant que ton API répond en JSON, la prochaine leçon approfondit exactement ce mécanisme de sérialisation, avec tous les cas particuliers que encoding/json doit gérer en production.
Commandes & code
net/http et API REST
Construire une API REST avec la seule bibliothèque standard, sans framework.
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
)
type Tache struct {
ID int `json:"id"`
Titre string `json:"titre"`
Faite bool `json:"faite"`
}
type Stockage struct {
mu sync.RWMutex
taches map[int]Tache
nextID int
}
func nouveauStockage() *Stockage {
return &Stockage{taches: make(map[int]Tache), nextID: 1}
}
func (s *Stockage) Lister() []Tache {
s.mu.RLock()
defer s.mu.RUnlock()
resultat := make([]Tache, 0, len(s.taches))
for _, t := range s.taches {
resultat = append(resultat, t)
}
return resultat
}
func (s *Stockage) Creer(titre string) Tache {
s.mu.Lock()
defer s.mu.Unlock()
t := Tache{ID: s.nextID, Titre: titre}
s.taches[t.ID] = t
s.nextID++
return t
}
func repondreJSON(w http.ResponseWriter, statut int, donnees any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statut)
json.NewEncoder(w).Encode(donnees)
}
func repondreErreur(w http.ResponseWriter, statut int, message string) {
repondreJSON(w, statut, map[string]string{"erreur": message})
}
// Handler via mux avec ServeMux (routage natif depuis Go 1.22, avec methodes/params)
func creerRoutes(s *Stockage) *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /taches", func(w http.ResponseWriter, r *http.Request) {
repondreJSON(w, http.StatusOK, s.Lister())
})
mux.HandleFunc("POST /taches", func(w http.ResponseWriter, r *http.Request) {
var payload struct {
Titre string `json:"titre"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
repondreErreur(w, http.StatusBadRequest, "corps JSON invalide")
return
}
if payload.Titre == "" {
repondreErreur(w, http.StatusUnprocessableEntity, "titre requis")
return
}
t := s.Creer(payload.Titre)
repondreJSON(w, http.StatusCreated, t)
})
mux.HandleFunc("GET /taches/{id}", func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
repondreErreur(w, http.StatusBadRequest, "id invalide")
return
}
s.mu.RLock()
t, ok := s.taches[id]
s.mu.RUnlock()
if !ok {
repondreErreur(w, http.StatusNotFound, "tache non trouvee")
return
}
repondreJSON(w, http.StatusOK, t)
})
return mux
}
// Middleware : fonction qui enveloppe un handler pour ajouter un comportement
func middlewareLog(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func main() {
stockage := nouveauStockage()
mux := creerRoutes(stockage)
handler := middlewareLog(mux)
log.Println("serveur demarre sur :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}Résumé
http.ServeMux(Go 1.22+) route par méthode et chemin, avec des paramètres{id}natifs.- Un middleware est simplement une fonction
func(http.Handler) http.Handler. - Toujours vérifier
Content-Type, valider le payload, retourner les bons codes HTTP. - Protéger l'état partagé (
Stockage) avecsync.RWMutexdès qu'il est accédé concurremment.
Exercices pratiques
Mission : une API de tâches qui plante sous charge en CI mais jamais en local
Objectif : Sécuriser un nouvel endpoint de suppression contre les races conditions, et éviter un piège de contention de verrou en production.
Contexte
Un développeur ajoute rapidement DELETE /taches/{id} à l'API de gestion de tâches en s'inspirant du style des autres handlers, mais sans relire attentivement comment Stockage protège son état. Le pipeline CI, qui lance les tests avec go test -race, échoue systématiquement sur ce nouvel endpoint, alors qu'aucun bug n'apparaît jamais en développement local avec un seul client à la fois.