
net/http est une bibliothèque standard très populaire en Go. Simple pour démarrer, elle vien avec un ensemble de fonctionnalités riches qui permettent de construire, tester et évoluer sans même y penser. Cet article explique comment les pièces s'emboitent. Quelle est la différence entre HandleFunc et HandlerFunc ? Comment écrire un middleware ? Si vous démarrez ou que vous ne comprenez pas complètement comment ça marche... C'est très simple; vérifez par vous-même.
TL;DR
http.Handler
est une interface et le bloc de base pour gérer les requêtes.httptest
aide avec unRecorder
et unServer
de test.HandlerFunc
transforme une fonction enHandler
. Un middleware est un decorator qui prend unHandler
et retourne un aHandler
.Handle
etHandleFunc
supportent le multiplexer denet/http
...
Les sections qui suivent presentent les concepts clé de net/http
; elles
expliquent pourquoi, pour quoi et illustrent à l'aide de nombreux exemples.
Handler, Server et Listener
http.Handler
est une interface Go utilisé comme bloc de base par net/http
afin de créer des Server
. L'interface est minimale et ne requière qu'une
seule méthode. La signature de l'interface consiste en la méthode suivante
ServeHTTP(http.ResponseWriter, *http.Request)
.
Résultat de cette construction, écrire un serveur web avec la bibliothèque
standard est très facile : (1) implémentez un Handler
(2) démarrez un serveur
qui l'utilise. Vous trouverez ci-dessous l'indémodable exemple Hello World!
qui fait exactement ça :
// Un simple serveur web
package main
import (
"fmt"
"net/http"
)
type helloworld struct{}
func (h *helloworld) ServeHTTP(w http.ResponseWriter,r *http.Request) {
fmt.Fprintln(w, "Hello World!")
}
func main() {
http.ListenAndServe(":8080", &helloworld{})
}
Comme vous pouvez le voir, le type helloworld
implemente l'interface
http.Handler
. La function main
appelle le helper ListenAndServe
qui crée
une instance du type helloworld
. C'est tout, vous pouvez lancer le code avec
la commande go run
et utiliser une outil tel que curl
pour faire appel à
lui :
curl 0.0.0.0:8080
Hello World!
Note dans l'exemple ci-dessus, le helper
ListenAndServe
crée unhttp.Server
par défaut de manière implicite. Si vous voulez créer et utiliser unServer
vous-même pour une configuration avancée telle que configurer un TTL pour vos requêtes ou gérer des arrêts propres, vous pouvez facilement le faire.
Client et Request
Maintenant que vous avez un serveur, vous devriez être capable de coder une
requête. La bibliothèque net/http
offre les types Request
et Client
ainsi
qu'un jeu de méthodes pour vous aider à les coder. Pour les utiliser, commencez
par la fonction requestHome
dans l'exemple qui suit :
// Simple Get on / request
package main
import (
"errors"
"io"
"fmt"
"net/http"
)
func requestHome() error {
request, _ := http.NewRequest("Get", "http://localhost:8080", nil)
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
return err
}
return readClose(response.Body)
}
func main() {
err := requestHome()
if err != nil {
fmt.Printf("**Error**: %v", err)
return
}
}
func readClose(r io.ReadCloser) error {
if r == nil {
return errors.New("empty")
}
defer r.Close()
data := make([]byte, 1024)
for {
n, err := r.Read(data)
if n > 0 {
fmt.Printf("%s", string(data[:n]))
}
if err == io.EOF {
return nil
}
if err != nil {
return err
}
}
}
Tester un Handler
La biblothèque net/http/httptest
vient avec un ensemble d'interfaces, types,
fonction et méthodes pour vous aider à tester des Handlers
sans avoir à
démarrer un listener web. Dans lexemple qui suit, vous verrez :
- Comment créer et utiliser un
httptest.Recorder
qui implémente l'interfacehttp.ResponseWriter
afin de tester un Handler. La méthodeResult()
qui retourne une*http.Response
une fois lesClient
/Request
utilisés - Comment créer une requête
http.Request
basique avec la fonctionhttptest.NewRequest()
pour tester vosHandler
.
Dans l'exemple ci-dessous, voux créez une Request
et un Recorder
pour
appeler un Handler
. Vous pouvez ensuite tester la Response
comme si vous
aviez fait un appel distant.
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHelloWorldHandler(t *testing.T) {
recorder := httptest.NewRecorder()
request := httptest.NewRequest("Get", "/", nil)
handler := &helloworld{}
handler.ServeHTTP(recorder, request)
result := recorder.Result()
if result.StatusCode != http.StatusOK {
t.Errorf(
"Status Code should be 200, current: %d",
result.StatusCode,
)
}
}
En fait, la biblothèque net/http/httptest
inclut également un constructeur
pour http.Server
. Vous trouverez ci-dessous le même test que le précédent qui
utilise cette fois NewServer()
. Dans ce cas, vous pouvez simplement lancer la
requête avec une commande client.Do()
sans avoir à utiliser httptest.Recorder
. Vous pouvez utiliser la manière de coder qui vous convient le mieux :
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestHelloWorldServer(t *testing.T) {
handler := &helloworld{}
server := httptest.NewServer(handler)
client := server.Client()
request, _ := http.NewRequest("Get", server.URL, nil)
response, err := client.Do(request)
if err != nil {
t.Errorf("Should not return %v", err)
}
if response.StatusCode != http.StatusOK {
t.Errorf(
"Status Code should be 200, current: %d",
response.StatusCode,
)
}
}
HandlerFunc et Closure
Vous avez surement déjà entendu que les fonctions en Go sont "fist class".
Résultat, si vous préférez utiliser une func
au lieu d'un type type
,
facile. La fonction http.HandlerFunc()
convertit une fonction qui partage
sa signature avec ServeHTTP
et la transforme en une intreface http.Handler
.
Un exemple vaut 1000 mots. Voici une réécriture du premier exemple helloworld
avec une fonction plutôt qu'un type :
// Un simple serveur web helloworld
package main
import (
"fmt"
"net/http"
)
func hello(w http.ResponseWriter,r *http.Request) {
fmt.Fprintln(w, "Hello World!")
}
func main() {
handler := http.HandlerFunc(hello) // transforms hello into an Handler
http.ListenAndServe(":8080", handler)
}
Si vous réduisez le code en utilisant une fonction anonyme que vous appelez directement, vous pouvez réduire le code comme ceci:
// Un simple serveur web helloworld
package main
import (
"fmt"
"net/http"
)
func main() {
http.ListenAndServe(
":8080",
http.HandlerFunc(
func(w http.ResponseWriter,r *http.Request) {
fmt.Fprintln(w, "Hello World!")
}),
)
}
Note souvenez-vous du proverbe Go "Clear is better than clever" et pensez aux lecteurs quand vous écrivez le code ci-dessous. Pensez que dans quelques semaines, ce lecteur ça sera peut-être vous.
Le principale bénéfice lorsqu'on utilise des fonctions, c'est la possibilité
d'utiliser des closures. Par exemple, si vous voulez que votre serveur web soit
multi-langue, vous pouvez créer une variable lang
que vous injecterez au
dans votre fonction au démarrage de votre serveur comme ceci :
// a simple hello world web server
package main
import (
"flag"
"fmt"
"net/http"
)
func translatedHello(lang string) func(http.ResponseWriter, *http.Request) {
msg := ""
switch lang {
case "fr":
msg = "Bonjour le Monde!"
default:
msg = "Hello World!"
}
return func(w http.ResponseWriter,r *http.Request) {
fmt.Fprintln(w, msg)
}
}
func main() {
lang := flag.String("lang", "en", "Server language")
flag.Parse()
http.ListenAndServe(
":8080",
http.HandlerFunc(translatedHello(*lang)),
)
}
Vous verrez d'autres façons de coder, y compris avec un container pour réaliser de l'injection de dépendances et ainsi interagir avec des services en backend ou faciliter vos tests.
Middleware
Considérons maintenant une fonction qui a un Handler
comme un paramètre et
renvoie un Handler
en sortie. Vous avez la base d'un Middleware. L'idée
derrière le middleware consiste à séparer un ensemble de fonctionnalités de
votre code, disons dans une fonction g
. De cette manière, si f
contient la
logique principale, alors g(f)
contient la logique principale augmentée des
aspects traités par g
.
Il existe des dizaines d'applications. Vous pouvez utiliser un middleware pour journaliser et comptabilier les temps de réponse des requêtes. Vous pouvez les utiliser pour l'authentification et gérer les autorisations. Vous pouvez maintenir des sessions, cacher des données, vérifier des headers comme les headers pour les CORS. Le bénéfice de cette approche est de pouvoir séparer les sujets et aussi de coder une fois et de l'utiliser partout.
Mais passons à la pratique. Voici un exemple de middleware simple :
// Un serveur web simple avec un middleware
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func hello(w http.ResponseWriter,r *http.Request) {
fmt.Fprintln(w, "Hello World!")
}
func logMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) {
starttime := time.Now()
next.ServeHTTP(w, r)
log.Printf(
"%dms %s %s",
time.Since(starttime).Milliseconds(),
r.Method,
r.URL.Path,
)
})
}
func main() {
handler := http.HandlerFunc(hello)
http.ListenAndServe(":8080", logMiddleware(handler))
}
Comme vous aurez déjà pu vous en rendre compte, il est possible d'enchainer les middleware les uns après les autres pour obtenir les fonctionnalités souhaitées. Pour aller plus loin, il est également possible d'utiliser une closure pour générer vos middleware.
Imaginons par exemple que vous vouliez personnaliser le format de vos logs, vous
pouvez tout à fait créer une fonction qui retourne une type
func(http.Handler) http.Handler
comme ci-dessous :
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello World!")
}
func logFormatMiddleware(format string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
starttime := time.Now()
next.ServeHTTP(w, r)
log.Printf(
format,
time.Since(starttime).Milliseconds(),
r.Method,
r.URL.Path,
)
})
}
}
func main() {
handler := http.HandlerFunc(hello)
http.ListenAndServe(
":8080",
logFormatMiddleware("%d [%s] \"%s\"")(handler))
}
Multiplexer, Handle et HandleFunc
Une chose manque encore à votre puzzle, la capacité de découper un Handler
en morceaux. Disons par exemple que vous voulez un certain fonctionnement
lorsqu'un utilisateur appelle /healthz
et donc associer un Handler à cette
partie et un autre lorsqu'un utilisateur appelle /compute
. C'est à ça que
sert un Multiplexer ou Mux. Le Multiplexer est lui-même un Handler dont le
comportement est modifié selon les Handler que vous lui avez associé.
C'est là que les fonction Handle
et HandleFunc
interviennent. De manière
simple, elles permettent de signifier:
- si l'appel concerne la route
/compute
, utilise le Handler compute - si l'appel concerne la route
/healthz
, utilise le Handler healthz
Une fois encore, voici un exemple pour bien comprendre:
package main
import (
"fmt"
"net/http"
)
type helloworld struct{}
func (h *helloworld) ServeHTTP(w http.ResponseWriter,r *http.Request) {
fmt.Fprintln(w, "Hello World!")
}
func goodbye(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Goodbye World!")
}
func main() {
mux := http.NewServeMux()
mux.Handle("/hello", helloworld)
mux.HandleFunc("/goodbye", goodbye)
http.ListenAndServe(
":8080",
mux)
}
Comme vous l'aurez deviné, Handle()
est utilisé pour enregistrer un
Handler
dans le multiplexer, alors aue HandleFunc()
est utilisé pour
enregistrer une HandlerFunc
. Par ailleurs :
- Il n'est généralement pas utile de créer un multiplexer car il existe un
multiplexer par défaut, Celui-ci est alimenté lorque les fonctions
http.Handle()
ethttp.HandleFunc()
sont utilisées au lieu des méthodesHandle()
etHandleFunc()
. Si le second paramètre de la fonctionhttp.ListenAndServe()
est nil, ce sera le multiplexer par défaut qui sera utilisé. - Si vous terminez par
/
le premier paramètre des fonctionsHandle()
etHandleFunc()
comme dansmux.Handle("/hello/", helloworld)
, alors toutes les routes sous la route définie comme/hello/gregory
seront gérées par leHandler
. Vous pourrez utiliserregexp
ou d'autres outils associés aux URL pour coder les différents comportements de votre application.
Utilitaires
Comme déjà évoqué, nons seulement la bibliothèque est bien écrite mais elle permet d'évoluer avec un nombre important de scenarios, comme d'utiliser des certificats TLS, des cookies, des keep-alive, des traces, le profiler et beaucoup plus. La bibliothèque est fantastique !
Parmi les outils extrêmement précieux, regardez la fonction
httputil.DumpRequest()
qui permet d'afficher le contenu d'une requête HTTP et
peut s'avérer très utile. Ci-dessous un exemple d'utilisation:
package main
import (
"fmt"
"net/http"
"net/http/httputil"
)
func dump(w http.ResponseWriter, r *http.Request) {
b, err := httputil.DumpRequest(r, false)
if err != nil {
fmt.Fprintf(w, "Error dumping request: %v", err)
}
fmt.Fprint(w, string(b))
}
func main() {
http.ListenAndServe(
":8080",
http.HandlerFunc(dump))
}
Voilà ce qui est retourné quand vous l'appelez:
curl 0.0.0.0:8080/goodbye
GET /goodbye HTTP/1.1
Host: 0.0.0.0:8080
Accept: */*
User-Agent: curl/7.64.1
Pour continuer
La plupart des applications écrites en Go s'appuient sur HTTP pour fournir des services comme une API, des appels à des services distant ou publier une application web. Si la bibliothèque standard ne fournit pas tout, elle fournit beaucoup. Prenez le temps de la découvrir et de l'utiliser.
Non pas que Gorilla, Chi, Gin, Echo, Go Kit, FastHTTP, HTTPRouter ne soient pas également d'une grande valeur. Simplement, les utiliser et payer le prix de la maintenance associée, y compris de rester compétent, n'est pas toujours nécessaire.
Prenez le temps d'apprendre net/http
;
lisez la doc et
lisez le code. Il y a
tellement de choses à en dire! Il ne fait pas de doute que certain des articles
à venir continueront à en creuser le fonctionnement.