Un serveur web en 6 lignes
La bibliothèque standard de Go inclut tout ce qu'il faut pour créer un serveur web. Pas besoin de framework externe :
Un handler HTTP en Go a la signature func(w http.ResponseWriter, r *http.Request) et ne renvoie aucune valeur. Avant de dérouler : comment envoie-t-on alors la réponse au client, et à quoi servent précisément w et r ?
Voir la réponse
Un handler Go ne renvoie rien (pas de valeur de retour). On envoie la réponse en écrivant dans w, le http.ResponseWriter : par exemple fmt.Fprintf(w, "Bonjour") ou w.Write([]byte("...")). w est le flux de sortie vers le client : on y écrit le corps, on règle le code de statut avec w.WriteHeader(...) et les en-têtes avec w.Header(). r, le *http.Request, contient tout ce qui vient du client : la méthode, l'URL, les paramètres (r.URL.Query()), les en-têtes, le corps. Modèle mental : on lit la requête dans r, on écrit la réponse dans w. Si on n'écrit rien dans w, le client reçoit une réponse 200 OK vide.
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Bonjour depuis Go !")
})
http.ListenAndServe(":8080", nil)
}
Lancez avec go run main.go et ouvrez http://localhost:8080. Votre serveur est en ligne.
En Python, il faudrait Flask ou Django. En JavaScript, Express. En Go, la bibliothèque standard suffit. Et ce serveur gère chaque requête dans sa propre goroutine : concurrence gratuite.
net/http la route vers le bon handler (dans sa propre goroutine), qui écrit la réponse dans w.En production, on configure le serveur. http.HandleFunc + ListenAndServe(":8080", nil) reposent sur un routeur global, pratique pour apprendre. En réel, on crée un http.ServeMux explicite et un http.Server avec des timeouts (sinon une connexion lente peut monopoliser une ressource indéfiniment) :
mux := http.NewServeMux()
mux.HandleFunc("/api/tasks", handleTasks)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
srv.ListenAndServe()
Plusieurs routes
Ajoutez autant de routes que nécessaire avec http.HandleFunc :
func main() {
http.HandleFunc("/", handleHome)
http.HandleFunc("/api/health", handleHealth)
http.HandleFunc("/api/users", handleUsers)
fmt.Println("Serveur démarré sur :8080")
http.ListenAndServe(":8080", nil)
}
func handleHome(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Page d'accueil")
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status": "ok"}`)
}
func handleUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"name": "Alice"}, {"name": "Bob"}]`)
}
Chaque handler reçoit deux paramètres :
w http.ResponseWriter: pour écrire la réponser *http.Request: la requête entrante (méthode, URL, body, headers)
JSON : encoding/json
Le package encoding/json convertit les structs Go en JSON et inversement :
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
// Struct → JSON (Marshal)
func handleGetUser(w http.ResponseWriter, r *http.Request) {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// JSON → Struct (Unmarshal)
func handleCreateUser(w http.ResponseWriter, r *http.Request) {
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "JSON invalide", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "Utilisateur créé : %s", user.Name)
}
Les tags `json:"name"` après chaque champ contrôlent le nom dans le JSON. Sans eux, Go utiliserait Name avec une majuscule.
Une API REST complète
Combinons tout pour créer une API de gestion de tâches :
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
var tasks = []Task{
{ID: 1, Title: "Apprendre Go", Done: false},
{ID: 2, Title: "Créer une API", Done: false},
}
func handleTasks(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case "GET":
json.NewEncoder(w).Encode(tasks)
case "POST":
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
http.Error(w, "JSON invalide", http.StatusBadRequest)
return
}
task.ID = len(tasks) + 1
tasks = append(tasks, task)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(task)
default:
http.Error(w, "Méthode non autorisée", http.StatusMethodNotAllowed)
}
}
Pour rester simple, cet exemple ne protège pas la variable tasks contre les accès concurrents. Or net/http traite chaque requête dans sa propre goroutine : deux POST simultanés provoqueraient une data race sur le slice. En production, on protégerait tasks avec un sync.Mutex (voir la leçon Goroutines et channels).
Cette API gère GET /api/tasks pour lister et POST /api/tasks pour créer. Testez-la avec curl :
# Lister les tâches
curl http://localhost:8080/api/tasks
# Créer une tâche
curl -X POST -d '{"title":"Première tâche"}' http://localhost:8080/api/tasks
À vous d'essayer (le serveur HTTP ne tourne pas ici, mais l'encodage JSON oui) :
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func main() {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
data, _ := json.Marshal(user)
fmt.Println(string(data))
}
A web server in 6 lines
Go's standard library includes everything you need to create a web server. No external framework required:
An HTTP handler in Go has the signature func(w http.ResponseWriter, r *http.Request) and returns no value. Before scrolling: how do you send the response to the client then, and what exactly are w and r for?
See the answer
A Go handler returns nothing (no return value). You send the response by writing into w, the http.ResponseWriter: for example fmt.Fprintf(w, "Hello") or w.Write([]byte("...")). w is the output stream to the client: you write the body there, set the status code with w.WriteHeader(...), and headers with w.Header(). r, the *http.Request, holds everything coming from the client: the method, URL, query parameters (r.URL.Query()), headers, and body. Mental model: you read the request from r, you write the response into w. If you write nothing into w, the client receives an empty 200 OK response.
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go!")
})
http.ListenAndServe(":8080", nil)
}
Run with go run main.go and open http://localhost:8080. Your server is live.
In Python, you'd need Flask or Django. In JavaScript, Express. In Go, the standard library is enough. And this server handles each request in its own goroutine — free concurrency.
net/http routes it to the right handler (in its own goroutine), which writes the response into w.In production, you configure the server. http.HandleFunc + ListenAndServe(":8080", nil) rely on a global router, handy for learning. In the real world you create an explicit http.ServeMux and an http.Server with timeouts (otherwise a slow connection can tie up a resource indefinitely):
mux := http.NewServeMux()
mux.HandleFunc("/api/tasks", handleTasks)
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
srv.ListenAndServe()
Multiple routes
Add as many routes as needed with http.HandleFunc:
func main() {
http.HandleFunc("/", handleHome)
http.HandleFunc("/api/health", handleHealth)
http.HandleFunc("/api/users", handleUsers)
fmt.Println("Server started on :8080")
http.ListenAndServe(":8080", nil)
}
func handleHome(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Home page")
}
func handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{"status": "ok"}`)
}
func handleUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `[{"name": "Alice"}, {"name": "Bob"}]`)
}
Each handler receives two parameters:
w http.ResponseWriter— to write the responser *http.Request— the incoming request (method, URL, body, headers)
JSON: encoding/json
The encoding/json package converts Go structs to JSON and vice versa:
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
// Struct → JSON (Marshal)
func handleGetUser(w http.ResponseWriter, r *http.Request) {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
// JSON → Struct (Unmarshal)
func handleCreateUser(w http.ResponseWriter, r *http.Request) {
var user User
err := json.NewDecoder(r.Body).Decode(&user)
if err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "User created: %s", user.Name)
}
The tags `json:"name"` after each field control the name in JSON. Without them, Go would use Name with a capital letter.
A complete REST API
Let's combine everything to create a task management API:
type Task struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
var tasks = []Task{
{ID: 1, Title: "Learn Go", Done: false},
{ID: 2, Title: "Create an API", Done: false},
}
func handleTasks(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case "GET":
json.NewEncoder(w).Encode(tasks)
case "POST":
var task Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
task.ID = len(tasks) + 1
tasks = append(tasks, task)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(task)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
To keep it simple, this example doesn't protect the tasks variable against concurrent access. But net/http serves each request in its own goroutine: two simultaneous POSTs would cause a data race on the slice. In production you'd guard tasks with a sync.Mutex (see the Goroutines and channels lesson).
This API handles GET /api/tasks to list and POST /api/tasks to create. Test it with curl:
# List tasks
curl http://localhost:8080/api/tasks
# Create a task
curl -X POST -d '{"title":"Test the API"}' http://localhost:8080/api/tasks
Try it yourself (the HTTP server doesn't run here, but JSON encoding does):
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func main() {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
data, _ := json.Marshal(user)
fmt.Println(string(data))
}
Le serveur HTTP ne fonctionne pas dans le Playground ni dans un sandbox en ligne. Installez Go localement pour tester : https://go.dev/dl/
🎯 Pratique
S'entraîner (clique pour ouvrir) :
✨ Prompt IA
Copiez ce prompt dans Claude ou ChatGPT :
Crée une API REST Go complète pour gérer des livres (CRUD). Utilise uniquement la bibliothèque standard. Ajoute la validation des données et des codes de statut HTTP appropriés.
💬 Ré-explique sans regarder
Sans relire le code de l'IA : décris de mémoire ce que fait un handler POST qui crée une ressource, de la lecture du r.Body jusqu'à la réponse. Quels packages et quel code de statut utilises-tu ?
json.NewDecoder(r.Body).Decode(&task) (package encoding/json) ; on vérifie l'erreur retournée et on répond http.StatusBadRequest (400) si le JSON est invalide ; en cas de succès on écrit Content-Type: application/json, on renvoie http.StatusCreated (201) avec w.WriteHeader, puis on encode la ressource créée avec json.NewEncoder(w).Encode(...). Le routage et la méthode viennent de net/http.⚖️ Juge le code de l'IA
L'IA te propose ce handler POST. Ton rôle de relecteur : l'accepter tel quel ou le rejeter, et dire pourquoi.
func handleCreateTask(w http.ResponseWriter, r *http.Request) {
var task Task
json.NewDecoder(r.Body).Decode(&task)
task.ID = len(tasks) + 1
tasks = append(tasks, task)
json.NewEncoder(w).Encode(task)
}
Decode est ignorée : si le client envoie un JSON invalide, task reste vide et on enregistre quand même une tâche vide au lieu de répondre 400 Bad Request. Réflexe Go : on ne jette jamais une error retournée. Il faut if err := json.NewDecoder(r.Body).Decode(&task); err != nil { http.Error(w, "JSON invalide", http.StatusBadRequest); return }. Bonus : pense aussi à w.WriteHeader(http.StatusCreated) avant d'encoder, sinon la réponse part en 200 alors qu'on a créé une ressource.🧠 Rappel libre
Sans remonter dans la leçon : quelle est la signature d'un handler HTTP en Go, et avec quelle fonction renvoies-tu un struct au format JSON dans la réponse ?
func(w http.ResponseWriter, r *http.Request) : w sert à écrire la réponse, r contient la requête entrante. Pour renvoyer un struct en JSON, on pose d'abord l'en-tête w.Header().Set("Content-Type", "application/json") puis on appelle json.NewEncoder(w).Encode(monStruct) (ou json.Marshal qui retourne un []byte).Votre API REST répond en JSON, mais comment savoir qu'elle ne casse pas au prochain changement ? Dans la dernière leçon, vous écrivez des tests avec le package testing intégré et vous mesurez les performances avec les benchmarks.
Leçon 11 : Tests et benchmarks →