Comment créer un serveur HTTP en Go

De Get Docs
Aller à :navigation, rechercher

L'auteur a sélectionné le Diversity in Tech Fund pour recevoir un don dans le cadre du programme Write for DOnations.

Introduction

De nombreux développeurs passent au moins une partie de leur temps à créer des serveurs pour distribuer du contenu sur Internet. Le Hypertext Transfer Protocol (HTTP) sert une grande partie de ce contenu, qu'il s'agisse d'une demande d'image de chat ou d'une demande de chargement du didacticiel que vous êtes en train de lire. La bibliothèque standard Go fournit une prise en charge intégrée pour la création d'un serveur HTTP pour servir votre contenu Web ou pour envoyer des requêtes HTTP à ces serveurs.

Dans ce didacticiel, vous allez créer un serveur HTTP à l'aide de la bibliothèque standard de Go, puis développer votre serveur pour lire les données de la chaîne de requête de la requête, du corps et des données du formulaire. Vous mettrez également à jour votre programme pour répondre à la demande avec vos propres en-têtes HTTP et codes d'état.

Conditions préalables

Pour suivre ce tutoriel, vous aurez besoin de :

Configuration du projet

Dans Go, la plupart des fonctionnalités HTTP sont fournies par le package net/http dans la bibliothèque standard, tandis que le reste de la communication réseau est fourni par le package net. Le package net/http inclut non seulement la possibilité de faire des requêtes HTTP, mais fournit également un serveur HTTP que vous pouvez utiliser pour gérer ces requêtes.

Dans cette section, vous allez créer un programme qui utilise la fonction http.ListenAndServe pour démarrer un serveur HTTP qui répond aux chemins de requête / et /hello. Ensuite, vous développerez ce programme pour exécuter plusieurs serveurs HTTP dans le même programme.

Avant d'écrire du code, cependant, vous devrez créer le répertoire de votre programme. De nombreux développeurs conservent leurs projets dans un répertoire pour les organiser. Dans ce didacticiel, vous utiliserez un répertoire nommé projects.

Commencez par créer le répertoire projects et accédez-y :

mkdir projects
cd projects

Ensuite, créez le répertoire de votre projet et accédez à ce répertoire. Dans ce cas, utilisez le répertoire httpserver :

mkdir httpserver
cd httpserver

Maintenant que vous avez créé le répertoire de votre programme et que vous êtes dans le répertoire httpserver, vous pouvez commencer à implémenter votre serveur HTTP.

Écouter les demandes et servir les réponses

Un serveur Go HTTP comprend deux composants principaux : le serveur qui écoute les requêtes provenant des clients HTTP et un ou plusieurs gestionnaires de requêtes qui répondront à ces requêtes. Dans cette section, vous commencerez par utiliser la fonction http.HandleFunc pour dire au serveur quelle fonction appeler pour gérer une requête au serveur. Ensuite, vous utiliserez la fonction http.ListenAndServe pour démarrer le serveur et lui dire d'écouter les nouvelles requêtes HTTP, puis de les servir à l'aide des fonctions de gestionnaire que vous avez configurées.

Maintenant, dans le répertoire httpserver que vous avez créé, utilisez nano, ou votre éditeur préféré, pour ouvrir le fichier main.go :

nano main.go

Dans le fichier main.go, vous allez créer deux fonctions, getRoot et getHello, pour agir comme vos fonctions de gestionnaire. Ensuite, vous allez créer une fonction main et l'utiliser pour configurer vos gestionnaires de requêtes avec la fonction http.HandleFunc en lui transmettant le chemin / pour le [X157X ] et le chemin /hello pour la fonction de gestionnaire getHello. Une fois que vous avez configuré vos gestionnaires, appelez la fonction http.ListenAndServe pour démarrer le serveur et écouter les requêtes.

Ajoutez le code suivant au fichier pour démarrer votre programme et configurer les gestionnaires :

main.go

package main

import (
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
)

func getRoot(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("got / request\n")
    io.WriteString(w, "This is my website!\n")
}
func getHello(w http.ResponseWriter, r *http.Request) {
    fmt.Printf("got /hello request\n")
    io.WriteString(w, "Hello, HTTP!\n")
}

Dans ce premier morceau de code, vous configurez le package pour votre programme Go, import les packages requis pour votre programme et créez deux fonctions : la fonction getRoot et la Fonction getHello. Ces deux fonctions ont la même signature de fonction, où elles acceptent les mêmes arguments : une valeur http.ResponseWriter et une valeur *http.Request. Cette signature de fonction est utilisée pour les fonctions de gestionnaire HTTP et est définie comme http.HandlerFunc. Lorsqu'une demande est adressée au serveur, il configure ces deux valeurs avec des informations sur la demande en cours, puis appelle la fonction de gestionnaire avec ces valeurs.

Dans un http.HandlerFunc, la valeur http.ResponseWriter (nommée w dans vos gestionnaires) est utilisée pour contrôler les informations de réponse qui sont réécrites au client qui a fait la demande, comme le corps de la réponse ou le code d'état. Ensuite, la valeur *http.Request (nommée r dans vos gestionnaires) est utilisée pour obtenir des informations sur la requête qui est arrivée au serveur, comme le corps envoyé dans le cas de une requête POST ou des informations sur le client qui a fait la requête.

Pour l'instant, dans vos deux gestionnaires HTTP, vous utilisez fmt.Printf pour imprimer lorsqu'une requête arrive pour la fonction de gestionnaire, puis vous utilisez http.ResponseWriter pour envoyer du texte au corps de la réponse. Le http.ResponseWriter est un io.Writer, ce qui signifie que vous pouvez utiliser tout ce qui est capable d'écrire sur cette interface pour écrire dans le corps de la réponse. Dans ce cas, vous utilisez la fonction io.WriteString pour écrire votre réponse dans le corps.

Maintenant, continuez à créer votre programme en lançant votre fonction main :

main.go

...
func main() {
    http.HandleFunc("/", getRoot)
    http.HandleFunc("/hello", getHello)

    err := http.ListenAndServe(":3333", nil)
...

Dans la fonction main, vous avez deux appels à la fonction http.HandleFunc. Chaque appel à la fonction configure une fonction de gestionnaire pour un chemin de requête spécifique dans le multiplexeur de serveur par défaut. Le multiplexeur de serveur est un http.Handler capable d'examiner un chemin de requête et d'appeler une fonction de gestionnaire donnée associée à ce chemin. Ainsi, dans votre programme, vous dites au multiplexeur de serveur par défaut d'appeler la fonction getRoot lorsque quelqu'un demande le chemin / et la fonction getHello lorsque quelqu'un demande le [X192X ] chemin.

Une fois les gestionnaires configurés, vous appelez la fonction http.ListenAndServe, qui indique au serveur HTTP global d'écouter les requêtes entrantes sur un port spécifique avec un http.Handler facultatif. Dans votre programme, vous dites au serveur d'écouter sur ":3333". En ne spécifiant pas d'adresse IP avant les deux-points, le serveur écoutera sur chaque adresse IP associée à votre ordinateur, et il écoutera sur le port 3333. Un port réseau, tel que 3333 ici, est un moyen pour un ordinateur d'avoir de nombreux programmes communiquant entre eux en même temps. Chaque programme utilise son propre port, donc lorsqu'un client se connecte à un port spécifique, l'ordinateur sait à quel programme l'envoyer. Si vous vouliez autoriser uniquement les connexions à localhost, le nom d'hôte pour l'adresse IP 127.0.0.1, vous pourriez plutôt dire 127.0.0.1:3333.

Votre fonction http.ListenAndServe transmet également une valeur nil pour le paramètre http.Handler. Cela indique à la fonction ListenAndServe que vous souhaitez utiliser le multiplexeur de serveur par défaut et non celui que vous avez configuré.

Le ListenAndServe est un appel bloquant, ce qui signifie que votre programme ne continuera à s'exécuter qu'après la fin de l'exécution de ListenAndServe. Cependant, ListenAndServe ne finira pas de s'exécuter tant que votre programme n'aura pas fini de s'exécuter ou que le serveur HTTP ne recevra pas l'ordre de s'arrêter. Même si ListenAndServe bloque et que votre programme n'inclut pas de moyen d'arrêter le serveur, il est toujours important d'inclure la gestion des erreurs car il existe plusieurs façons d'appeler ListenAndServe peut échouer. Ajoutez donc la gestion des erreurs à votre ListenAndServe dans la fonction main comme indiqué :

main.go

...

func main() {
    ...
    err := http.ListenAndServe(":3333", nil)
  if errors.Is(err, http.ErrServerClosed) {
        fmt.Printf("server closed\n")
    } else if err != nil {
        fmt.Printf("error starting server: %s\n", err)
        os.Exit(1)
    <^>}
}

La première erreur que vous recherchez, http.ErrServerClosed, est renvoyée lorsque le serveur est invité à s'arrêter ou à se fermer. Il s'agit généralement d'une erreur attendue car vous allez arrêter le serveur vous-même, mais elle peut également être utilisée pour montrer pourquoi le serveur s'est arrêté dans la sortie. Dans la deuxième vérification d'erreur, vous recherchez toute autre erreur. Si cela se produit, il imprimera l'erreur à l'écran puis quittera le programme avec un code d'erreur de 1 en utilisant la fonction os.Exit.

Une erreur que vous pouvez rencontrer lors de l'exécution de votre programme est l'erreur address already in use. Cette erreur peut être renvoyée lorsque ListenAndServe est incapable d'écouter sur l'adresse ou le port que vous avez fourni car un autre programme l'utilise déjà. Parfois, cela peut se produire si le port est couramment utilisé et qu'un autre programme de votre ordinateur l'utilise, mais cela peut également se produire si vous exécutez plusieurs copies de votre propre programme plusieurs fois. Si vous voyez cette erreur pendant que vous travaillez sur ce didacticiel, assurez-vous d'avoir arrêté votre programme à partir d'une étape précédente avant de l'exécuter à nouveau.

Remarque : Si vous voyez l'erreur address already in use et que vous n'avez pas d'autre copie de votre programme en cours d'exécution, cela peut signifier qu'un autre programme l'utilise. Si cela se produit, partout où vous voyez 3333 mentionné dans ce didacticiel, remplacez-le par un autre nombre supérieur à 1024 et inférieur à 65535, tel que 3334, puis réessayez. Si vous voyez toujours l'erreur, vous devrez peut-être continuer à essayer de trouver un port qui n'est pas utilisé. Une fois que vous avez trouvé un port qui fonctionne, utilisez-le pour toutes vos commandes dans ce didacticiel.


Maintenant que votre code est prêt, enregistrez votre fichier main.go et exécutez votre programme en utilisant go run. Contrairement à d'autres programmes Go que vous avez peut-être écrits, ce programme ne se fermera pas tout de suite de lui-même. Une fois le programme exécuté, passez aux commandes suivantes :

go run main.go

Étant donné que votre programme est toujours en cours d'exécution dans votre terminal, vous devrez ouvrir un deuxième terminal pour interagir avec votre serveur. Lorsque vous voyez des commandes ou une sortie avec la même couleur que la commande ci-dessous, cela signifie de l'exécuter dans ce deuxième terminal.

Dans ce deuxième terminal, utilisez le programme curl pour faire une requête HTTP à votre serveur HTTP. curl est un utilitaire couramment installé par défaut sur de nombreux systèmes qui peut faire des requêtes à des serveurs de différents types. Pour ce didacticiel, vous l'utiliserez pour effectuer des requêtes HTTP. Votre serveur écoute les connexions sur le port de votre ordinateur 3333, vous devrez donc faire votre demande à localhost sur ce même port :

curl http://localhost:3333

La sortie ressemblera à ceci :

OutputThis is my website!

Dans la sortie, vous verrez la réponse This is my website! de la fonction getRoot, car vous avez accédé au chemin / sur votre serveur HTTP.

Maintenant, dans ce même terminal, faites une demande au même hôte et port, mais ajoutez le chemin /hello à la fin de votre commande curl :

curl http://localhost:3333/hello

Votre sortie ressemblera à ceci :

OutputHello, HTTP!

Cette fois, vous verrez la réponse Hello, HTTP! de la fonction getHello.

Si vous vous référez au terminal dans lequel votre fonction de serveur HTTP est exécutée, vous avez maintenant deux lignes de sortie de votre serveur. Un pour la requête / et un autre pour la requête /hello :

Outputgot / request
got /hello request

Étant donné que le serveur continuera de fonctionner jusqu'à la fin de l'exécution du programme, vous devrez l'arrêter vous-même. Pour ce faire, appuyez sur CONTROL+C pour envoyer à votre programme le signal d'interruption pour l'arrêter.

Dans cette section, vous avez créé un programme de serveur HTTP, mais il utilise le multiplexeur de serveur par défaut et le serveur HTTP par défaut. L'utilisation de valeurs par défaut ou globales peut entraîner des bogues difficiles à dupliquer car plusieurs parties de votre programme peuvent les mettre à jour à des moments différents et variables. Si cela conduit à un état incorrect, il peut être difficile de retrouver le bogue car il peut n'exister que si certaines fonctions sont appelées dans un ordre particulier. Donc, pour éviter ce problème, vous allez mettre à jour votre serveur pour utiliser un multiplexeur de serveur que vous créez vous-même dans la section suivante.

Gestionnaires de demandes de multiplexage

Lorsque vous avez démarré votre serveur HTTP dans la dernière section, vous avez transmis à la fonction ListenAndServe une valeur nil pour le paramètre http.Handler car vous utilisiez le multiplexeur de serveur par défaut. Étant donné que http.Handler est une interface, il est possible de créer votre propre struct qui implémente l'interface. Mais parfois, vous n'avez besoin que d'un http.Handler de base qui appelle une seule fonction pour un chemin de requête spécifique, comme le multiplexeur de serveur par défaut. Dans cette section, vous mettrez à jour votre programme pour utiliser http.ServeMux, un multiplexeur de serveur et l'implémentation http.Handler fournie par le package net/http, que vous pouvez utiliser dans ces cas. .

Le http.ServeMux struct peut être configuré de la même manière que le multiplexeur de serveur par défaut, il n'y a donc pas beaucoup de mises à jour que vous devez apporter à votre programme pour commencer à utiliser le vôtre au lieu d'un défaut global. Pour mettre à jour votre programme pour utiliser un http.ServeMux, ouvrez à nouveau votre fichier main.go et mettez à jour votre programme pour utiliser votre propre http.ServeMux :

main.go

...

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", getRoot)
    mux.HandleFunc("/hello", getHello)

    err := http.ListenAndServe(":3333", mux)
    
    ...
}

Dans cette mise à jour, vous avez créé un nouveau http.ServeMux à l'aide du constructeur http.NewServeMux et l'avez affecté à la variable mux. Après cela, il vous suffisait de mettre à jour les appels http.HandleFunc pour utiliser la variable mux au lieu d'appeler le package http. Enfin, vous avez mis à jour l'appel à http.ListenAndServe pour lui fournir le http.Handler que vous avez créé (mux) au lieu d'une valeur nil.

Vous pouvez maintenant exécuter à nouveau votre programme en utilisant go run :

go run main.go

Votre programme continuera à fonctionner comme il l'a fait la dernière fois, vous devrez donc exécuter des commandes pour interagir avec le serveur dans un autre terminal. Tout d'abord, utilisez curl pour redemander le chemin / :

curl http://localhost:3333

La sortie ressemblera à ce qui suit :

OutputThis is my website!

Vous verrez que cette sortie est la même qu'avant.

Ensuite, exécutez la même commande qu'auparavant pour le chemin /hello :

curl http://localhost:3333/hello

La sortie ressemblera à ceci :

OutputHello, HTTP!

La sortie de ce chemin est également la même qu'avant.

Enfin, si vous vous référez au terminal d'origine, vous verrez la sortie pour les requêtes / et /hello comme avant :

Outputgot / request
got /hello request

La mise à jour que vous avez apportée au programme est fonctionnellement la même, mais cette fois, vous utilisez votre propre http.Handler au lieu de celui par défaut.

Enfin, appuyez à nouveau sur CONTROL+C pour quitter votre programme serveur.

Exécution de plusieurs serveurs en même temps

En plus d'utiliser votre propre http.Handler, le package Go net/http vous permet également d'utiliser un serveur HTTP autre que celui par défaut. Parfois, vous souhaiterez peut-être personnaliser le fonctionnement du serveur ou exécuter plusieurs serveurs HTTP dans le même programme à la fois. Par exemple, vous pouvez avoir un site Web public et un site Web d'administration privé que vous souhaitez exécuter à partir du même programme. Comme vous ne pouvez avoir qu'un seul serveur HTTP par défaut, vous ne pourrez pas le faire avec celui par défaut. Dans cette section, vous allez mettre à jour votre programme pour utiliser deux valeurs http.Server fournies par le package net/http dans des cas comme ceux-ci — lorsque vous voulez plus de contrôle sur le serveur ou avez besoin de plusieurs serveurs à le même temps.

Dans votre fichier main.go, vous configurerez plusieurs serveurs HTTP à l'aide de http.Server. Vous mettrez également à jour vos fonctions de gestionnaire pour accéder au context.Context pour le *http.Request entrant. Cela vous permettra de définir de quel serveur provient la requête dans une variable context.Context, afin que vous puissiez imprimer le serveur dans la sortie de la fonction de gestionnaire.

Ouvrez à nouveau votre fichier main.go et mettez-le à jour comme indiqué :

main.go

package main

import (
    // Note: Also remove the 'os' import.
    "context"
    "errors"
    "fmt"
    "io"
    "net"
    "net/http"
)

const keyServerAddr = "serverAddr"

func getRoot(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got / request\n", ctx.Value(keyServerAddr))
    io.WriteString(w, "This is my website!\n")
}
func getHello(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))
    io.WriteString(w, "Hello, HTTP!\n")
}

Dans la mise à jour du code ci-dessus, vous avez mis à jour l'instruction import pour inclure les packages nécessaires à la mise à jour. Ensuite, vous avez créé une valeur const string appelée keyServerAddr pour servir de clé pour la valeur d'adresse du serveur HTTP dans le contexte http.Request. Enfin, vous avez mis à jour vos fonctions getRoot et getHello pour accéder à la valeur context.Context de http.Request. Une fois que vous avez la valeur, vous incluez l'adresse du serveur HTTP dans la sortie fmt.Printf afin que vous puissiez voir lequel des deux serveurs a traité la requête HTTP.

Maintenant, commencez à mettre à jour votre fonction main en ajoutant la première de vos deux valeurs http.Server :

main.go

...
func main() {
    ...
    mux.HandleFunc("/hello", getHello)

    ctx, cancelCtx := context.WithCancel(context.Background())
    serverOne := &http.Server{
        Addr:    ":3333",
        Handler: mux,
        BaseContext: func(l net.Listener) context.Context {
            ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
            return ctx
        },
    }

Dans le code mis à jour, la première chose que vous avez faite est de créer une nouvelle valeur context.Context avec une fonction disponible, cancelCtx, pour annuler le contexte. Ensuite, vous définissez votre valeur serverOne http.Server. Cette valeur est très similaire au serveur HTTP que vous avez déjà utilisé, mais au lieu de transmettre l'adresse et le gestionnaire à la fonction http.ListenAndServe, vous les définissez comme Addr et Handler valeurs du http.Server.

Un autre changement consiste à ajouter une fonction BaseContext. BaseContext est un moyen de modifier des parties de context.Context que les fonctions de gestionnaire reçoivent lorsqu'elles appellent la méthode Context de *http.Request. Dans le cas de votre programme, vous ajoutez l'adresse sur laquelle le serveur écoute (l.Addr().String()) au contexte avec la clé serverAddr, qui sera ensuite imprimée sur la sortie de la fonction de gestionnaire.

Ensuite, définissez votre deuxième serveur, serverTwo :

main.go

...

func main() {
    ...
    serverOne := &http.Server {
        ...
    }

    serverTwo := &http.Server{
        Addr:    ":4444",
        Handler: mux,
        BaseContext: func(l net.Listener) context.Context {
            ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
            return ctx
        },
    }

Ce serveur est défini de la même manière que le premier serveur, sauf qu'au lieu de :3333 pour le champ Addr, vous le définissez sur :4444. De cette façon, un serveur écoutera les connexions sur le port 3333 et le second serveur écoutera sur le port 4444.

Maintenant, mettez à jour votre programme pour démarrer le premier serveur, serverOne, dans une goroutine :

main.go

...

func main() {
    ...
    serverTwo := &http.Server {
        ...
    }

    go func() {
        err := serverOne.ListenAndServe()
        if errors.Is(err, http.ErrServerClosed) {
            fmt.Printf("server one closed\n")
        } else if err != nil {
            fmt.Printf("error listening for server one: %s\n", err)
        }
        cancelCtx()
    }()

À l'intérieur de la goroutine, vous démarrez le serveur avec ListenAndServe, comme avant, mais cette fois vous n'avez pas besoin de fournir de paramètres à la fonction comme vous l'avez fait avec http.ListenAndServe car le [ Les valeurs X195X] ont déjà été configurées. Ensuite, vous effectuez la même gestion des erreurs qu'auparavant. À la fin de la fonction, vous appelez cancelCtx pour annuler le contexte fourni aux gestionnaires HTTP et aux deux fonctions serveur BaseContext. De cette façon, si le serveur se termine pour une raison quelconque, le contexte se terminera également.

Enfin, mettez à jour votre programme pour démarrer également le deuxième serveur dans une goroutine :

main.go

...

func main() {
    ...
    go func() {
        ...
    }()
    go func() {
        err := serverTwo.ListenAndServe()
        if errors.Is(err, http.ErrServerClosed) {
            fmt.Printf("server two closed\n")
        } else if err != nil {
            fmt.Printf("error listening for server two: %s\n", err)
        }
        cancelCtx()
    }()

    <-ctx.Done()
}

Cette goroutine est fonctionnellement la même que la première, elle démarre simplement serverTwo au lieu de serverOne. Cette mise à jour inclut également la fin de la fonction main où vous lisez depuis le canal ctx.Done avant de revenir de la fonction main. Cela garantit que votre programme continuera de fonctionner jusqu'à ce que l'une des goroutines du serveur se termine et que cancelCtx soit appelé. Une fois le contexte terminé, votre programme se fermera.

Enregistrez et fermez le fichier lorsque vous avez terminé.

Exécutez votre serveur à l'aide de la commande go run :

go run main.go

Votre programme continuera de s'exécuter à nouveau, donc dans votre deuxième terminal, exécutez les commandes curl pour demander le chemin / et le chemin /hello du serveur écoutant sur [X182X ], identique aux requêtes précédentes :

curl http://localhost:3333
curl http://localhost:3333/hello

La sortie ressemblera à ceci :

OutputThis is my website!
Hello, HTTP!

Dans la sortie, vous verrez les mêmes réponses que vous avez vues auparavant.

Maintenant, exécutez à nouveau ces mêmes commandes, mais cette fois utilisez le port 4444, celui qui correspond à serverTwo dans votre programme :

curl http://localhost:4444
curl http://localhost:4444/hello

La sortie ressemblera à ce qui suit :

OutputThis is my website!
Hello, HTTP!

Vous verrez le même résultat pour ces requêtes que pour les requêtes sur le port 3333 servi par serverOne.

Enfin, regardez le terminal d'origine sur lequel votre serveur s'exécute :

Output[::]:3333: got / request
[::]:3333: got /hello request
[::]:4444: got / request
[::]:4444: got /hello request

La sortie ressemble à ce que vous avez vu auparavant, mais cette fois, elle montre le serveur qui a répondu à la requête. Les deux premières requêtes indiquent qu'elles proviennent du serveur écoutant sur le port 3333 (serverOne), et les deux autres requêtes proviennent du serveur écoutant sur le port 4444 (serverTwo). Ce sont les valeurs extraites de la valeur serverAddr de BaseContext.

Votre sortie peut également être légèrement différente de la sortie ci-dessus selon que votre ordinateur est configuré pour utiliser IPv6 ou non. Si c'est le cas, vous verrez la même sortie que ci-dessus. Sinon, vous verrez 0.0.0.0 au lieu de [::]. La raison en est que votre ordinateur communiquera avec lui-même via IPv6 s'il est configuré, et [::] est la notation IPv6 pour 0.0.0.0.

Une fois que vous avez terminé, utilisez à nouveau CONTROL+C pour arrêter le serveur.

Dans cette section, vous avez créé un nouveau programme de serveur HTTP en utilisant http.HandleFunc et http.ListenAndServe pour exécuter et configurer le serveur par défaut. Ensuite, vous l'avez mis à jour pour utiliser un http.ServeMux pour le http.Handler au lieu du multiplexeur de serveur par défaut. Enfin, vous avez mis à jour votre programme pour utiliser http.Server pour exécuter plusieurs serveurs HTTP dans le même programme.

Bien que vous ayez un serveur HTTP en cours d'exécution, il n'est pas très interactif. Vous pouvez ajouter de nouveaux chemins auxquels il répond, mais il n'y a pas vraiment de moyen pour les utilisateurs d'interagir avec lui après cela. Le protocole HTTP comprend un certain nombre de façons dont les utilisateurs peuvent interagir avec un serveur HTTP au-delà des chemins. Dans la section suivante, vous mettrez à jour votre programme pour prendre en charge le premier d'entre eux : les valeurs de chaîne de requête.

Inspecter la chaîne de requête d'une demande

L'un des moyens par lesquels un utilisateur peut influencer la réponse HTTP qu'il reçoit d'un serveur HTTP consiste à utiliser la chaîne de requête [1]. La chaîne de requête est un ensemble de valeurs ajoutées à la fin d'une URL. Il commence par un caractère ?, avec des valeurs supplémentaires ajoutées en utilisant & comme délimiteur. Les valeurs de chaîne de requête sont couramment utilisées pour filtrer ou personnaliser les résultats qu'un serveur HTTP envoie en réponse. Par exemple, un serveur peut utiliser une valeur results pour permettre à un utilisateur de spécifier quelque chose comme results=10 pour dire qu'il aimerait voir 10 éléments dans sa liste de résultats.

Dans cette section, vous allez mettre à jour votre fonction de gestionnaire getRoot pour utiliser sa valeur *http.Request pour accéder aux valeurs de chaîne de requête et les imprimer dans la sortie.

Tout d'abord, ouvrez votre fichier main.go et mettez à jour la fonction getRoot pour accéder à la chaîne de requête avec la méthode r.URL.Query. Ensuite, mettez à jour la méthode main pour supprimer serverTwo et tout son code associé puisque vous n'en aurez plus besoin :

main.go

...

func getRoot(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    hasFirst := r.URL.Query().Has("first")
    first := r.URL.Query().Get("first")
    hasSecond := r.URL.Query().Has("second")
    second := r.URL.Query().Get("second")

    fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s\n",
        ctx.Value(keyServerAddr),
        hasFirst, first,
        hasSecond, second)
    io.WriteString(w, "This is my website!\n")
}
...

Dans la fonction getRoot, vous utilisez le champ r.URL de getRoot pour accéder aux propriétés de l'URL demandée. Ensuite, vous utilisez la méthode Query du champ r.URL pour accéder aux valeurs de chaîne de requête de la requête. Une fois que vous accédez aux valeurs de la chaîne de requête, vous pouvez utiliser deux méthodes pour interagir avec les données. La méthode Has renvoie une valeur bool spécifiant si la chaîne de requête a une valeur avec la clé fournie, telle que first. Ensuite, la méthode Get renvoie un string avec la valeur de la clé fournie.

En théorie, vous pouvez toujours utiliser la méthode Get pour récupérer les valeurs de la chaîne de requête, car elle renverra toujours soit la valeur réelle de la clé donnée, soit une chaîne vide si la clé n'existe pas. Pour de nombreuses utilisations, cela suffit, mais dans certains cas, vous souhaiterez peut-être connaître la différence entre un utilisateur fournissant une valeur vide ou ne fournissant pas de valeur du tout. En fonction de votre cas d'utilisation, vous souhaiterez peut-être savoir si un utilisateur a fourni une valeur filter de rien, ou s'il n'a fourni aucune valeur filter. S'ils ont fourni une valeur filter de rien, vous voudrez peut-être la traiter comme "ne me montrez rien", alors que ne pas fournir une valeur filter signifierait "montrez-moi tout". L'utilisation de Has et Get vous permettra de faire la différence entre ces deux cas.

Dans votre fonction getRoot, vous avez également mis à jour la sortie pour afficher les valeurs Has et Get pour les valeurs de chaîne de requête first et second. .

Maintenant, mettez à jour votre fonction main pour recommencer à utiliser un serveur :

main.go

...

func main() {
    ...
    mux.HandleFunc("/hello", getHello)
    
    ctx := context.Background()
    server := &http.Server{
        Addr:    ":3333",
        Handler: mux,
        BaseContext: func(l net.Listener) context.Context {
            ctx = context.WithValue(ctx, keyServerAddr, l.Addr().String())
            return ctx
        },
    }

    err := server.ListenAndServe()
    if errors.Is(err, http.ErrServerClosed) {
        fmt.Printf("server closed\n")
    } else if err != nil {
        fmt.Printf("error listening for server: %s\n", err)
    }
}

Dans la fonction main, vous avez supprimé les références à serverTwo et déplacé l'exécution du server (anciennement serverOne) hors d'une goroutine vers le [ X152X], similaire à la façon dont vous exécutiez http.ListenAndServe plus tôt. Vous pouvez également le remplacer par http.ListenAndServe au lieu d'utiliser une valeur http.Server puisque vous n'avez qu'un seul serveur en cours d'exécution, mais en utilisant http.Server, vous aurez moins à mettre à jour si vous vouliez apporter des personnalisations supplémentaires au serveur à l'avenir.

Maintenant, une fois que vous avez enregistré vos modifications, utilisez go run pour relancer votre programme :

go run main.go

Votre serveur recommencera à fonctionner, alors revenez à votre deuxième terminal pour exécuter une commande curl avec une chaîne de requête. Dans cette commande, vous devrez entourer votre URL de guillemets simples ('), sinon le shell de votre terminal peut interpréter le symbole & dans la chaîne de requête comme le "exécuter cette commande en arrière-plan ” caractéristique que de nombreux shells incluent. Dans l'URL, incluez une valeur de first=1 pour first et second= pour second :

curl 'http://localhost:3333?first=1&second='

La sortie ressemblera à ceci :

OutputThis is my website!

Vous verrez que la sortie de la commande curl n'a pas changé par rapport aux demandes précédentes.

Cependant, si vous revenez à la sortie de votre programme serveur, vous verrez que la nouvelle sortie inclut les valeurs de la chaîne de requête :

Output[::]:3333: got / request. first(true)=1, second(true)=

La sortie pour la valeur de la chaîne de requête first montre que la méthode Has a renvoyé true parce que first a une valeur, et aussi que Get a renvoyé la valeur de 1. La sortie pour second montre que Has a renvoyé true parce que second a été inclus, mais la méthode Get n'a rien renvoyé d'autre que une chaîne vide. Vous pouvez également essayer de faire différentes requêtes en ajoutant et en supprimant first et second ou en définissant des valeurs différentes pour voir comment cela modifie la sortie de ces fonctions.

Une fois que vous avez terminé, appuyez sur CONTROL+C pour arrêter votre serveur.

Dans cette section, vous avez mis à jour votre programme pour utiliser à nouveau un seul http.Server, mais vous avez également ajouté la prise en charge de la lecture des valeurs first et second à partir de la chaîne de requête pour le [ X178X] fonction de gestionnaire.

Cependant, l'utilisation de la chaîne de requête n'est pas le seul moyen pour les utilisateurs de fournir une entrée à un serveur HTTP. Une autre façon courante d'envoyer des données à un serveur consiste à inclure des données dans le corps de la requête. Dans la section suivante, vous mettrez à jour votre programme pour lire le corps d'une requête à partir des données *http.Request.

Lecture d'un corps de requête

Lors de la création d'une API basée sur HTTP, telle qu'une API REST, un utilisateur peut avoir besoin d'envoyer plus de données que ce qui peut être inclus dans les limites de longueur d'URL, ou votre page peut avoir besoin de recevoir des données qui ne concernent pas comment les données doivent être interprétées, telles que les filtres ou les limites de résultats. Dans ces cas, il est courant d'inclure des données dans le corps de la requête et de l'envoyer avec une requête HTTP POST ou PUT.

Dans un Go http.HandlerFunc, la valeur *http.Request est utilisée pour accéder aux informations sur la demande entrante, et elle inclut également un moyen d'accéder au corps de la demande avec le champ Body. Dans cette section, vous allez mettre à jour votre fonction de gestionnaire getRoot pour lire le corps de la requête.

Pour mettre à jour votre méthode getRoot, ouvrez votre fichier main.go et mettez-le à jour pour utiliser ioutil.ReadAll pour lire le champ de requête r.Body :

main.go

package main

import (
    ...
    "io/ioutil"
    ...
)

...

func getRoot(w http.ResponseWriter, r *http.Request) {
    ...
    second := r.URL.Query().Get("second")

    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        fmt.Printf("could not read body: %s\n", err)
    }

    fmt.Printf("%s: got / request. first(%t)=%s, second(%t)=%s, body:\n%s\n",
        ctx.Value(keyServerAddr),
        hasFirst, first,
        hasSecond, second,
        body)
    io.WriteString(w, "This is my website!\n")
}

...

Dans cette mise à jour, vous utilisez la fonction ioutil.ReadAll pour lire la propriété r.Body du *http.Request afin d'accéder au corps de la requête. La fonction ioutil.ReadAll est une fonction utilitaire qui lit les données d'un io.Reader jusqu'à ce qu'il rencontre une erreur ou la fin des données. Comme r.Body est un io.Reader, vous pouvez l'utiliser pour lire le corps. Une fois que vous avez lu le corps, vous avez également mis à jour le fmt.Printf pour l'imprimer sur la sortie.

Après avoir enregistré vos mises à jour, lancez votre serveur à l'aide de la commande go run :

go run main.go

Étant donné que le serveur continuera de fonctionner jusqu'à ce que vous l'arrêtiez, accédez à votre autre terminal pour effectuer une requête POST en utilisant curl avec l'option -X POST et un corps en utilisant -d. Vous pouvez également utiliser les valeurs de chaîne de requête first et second d'avant :

curl -X POST -d 'This is the body' 'http://localhost:3333?first=1&second='

Votre sortie ressemblera à ceci :

OutputThis is my website!

La sortie de votre fonction de gestionnaire est la même, mais vous verrez que les journaux de votre serveur ont été à nouveau mis à jour :

Output[::]:3333: got / request. first(true)=1, second(true)=, body:
This is the body

Dans les journaux du serveur, vous verrez les valeurs de chaîne de requête d'avant, mais maintenant vous verrez également les données This is the body envoyées par la commande curl.

Maintenant, arrêtez le serveur en appuyant sur CONTROL+C.

Dans cette section, vous avez mis à jour votre programme pour lire le corps d'une requête dans une variable que vous avez imprimée dans la sortie. En combinant la lecture du corps de cette façon avec d'autres fonctionnalités, telles que encoding/json pour démarshaler un corps JSON dans les données Go, vous serez en mesure de créer des API avec lesquelles vos utilisateurs peuvent interagir d'une manière qu'ils familier avec d'autres API.

Cependant, toutes les données envoyées par un utilisateur ne se présentent pas sous la forme d'une API. De nombreux sites Web ont des formulaires qu'ils demandent à leurs utilisateurs de remplir, donc dans la section suivante, vous mettrez à jour votre programme pour lire les données du formulaire en plus du corps de la requête et de la chaîne de requête que vous avez déjà.

Récupération des données de formulaire

Pendant longtemps, l'envoi de données à l'aide de formulaires était le moyen standard pour les utilisateurs d'envoyer des données à un serveur HTTP et d'interagir avec un site Web. Les formulaires ne sont plus aussi populaires aujourd'hui qu'ils l'étaient par le passé, mais ils ont encore de nombreuses utilisations pour permettre aux utilisateurs de soumettre des données à un site Web. La valeur *http.Request dans http.HandlerFunc fournit également un moyen d'accéder à ces données, de la même manière qu'elle permet d'accéder à la chaîne de requête et au corps de la requête. Dans cette section, vous allez mettre à jour votre programme getHello pour recevoir le nom d'un utilisateur à partir d'un formulaire et lui répondre avec son nom.

Ouvrez votre main.go et mettez à jour la fonction getHello pour utiliser la méthode PostFormValue de *http.Request :

main.go

...

func getHello(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))

    myName := r.PostFormValue("myName")
    if myName == "" {
        myName = "HTTP"
    }
    io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
}

...

Maintenant, dans votre fonction getHello, vous lisez les valeurs de formulaire envoyées à votre fonction de gestionnaire et recherchez une valeur nommée myName. Si la valeur est introuvable ou si la valeur trouvée est une chaîne vide, vous définissez la variable myName sur une valeur par défaut de HTTP afin que la page n'affiche pas un nom vide. Ensuite, vous avez mis à jour la sortie à l'utilisateur pour afficher le nom qu'il a envoyé, ou HTTP s'il n'a pas envoyé de nom.

Pour exécuter votre serveur avec ces mises à jour, enregistrez vos modifications et exécutez-le à l'aide de go run :

go run main.go

Maintenant, dans votre deuxième terminal, utilisez curl avec l'option -X POST à l'URL /hello, mais cette fois au lieu d'utiliser -d pour fournir un corps de données , utilisez l'option -F 'myName=Sammy' pour fournir des données de formulaire avec un champ myName avec la valeur Sammy :

curl -X POST -F 'myName=Sammy' 'http://localhost:3333/hello'

La sortie ressemblera à ceci :

OutputHello, Sammy!

Dans la sortie ci-dessus, vous verrez le message d'accueil Hello, Sammy! attendu, car le formulaire que vous avez envoyé avec curl indique que myName est Sammy.

La méthode r.PostFormValue que vous utilisez dans la fonction getHello pour récupérer la valeur du formulaire myName est une méthode spéciale qui inclut uniquement les valeurs publiées à partir d'un formulaire dans le corps d'une demande . Cependant, une méthode r.FormValue est également disponible qui inclut à la fois le corps du formulaire et toutes les valeurs de la chaîne de requête. Ainsi, si vous avez utilisé r.FormValue("myName"), vous pouvez également supprimer l'option -F et inclure myName=Sammy dans la chaîne de requête pour voir Sammy renvoyé également. Si vous l'avez fait sans le changement de r.FormValue, cependant, vous verriez la réponse par défaut HTTP pour le nom. Faire attention à l'endroit où vous récupérez ces valeurs peut éviter les conflits potentiels dans les noms ou les bogues difficiles à localiser. Il est utile d'être plus strict et d'utiliser le r.PostFormValue à moins que vous ne vouliez avoir la flexibilité de le mettre également dans la chaîne de requête.

Si vous regardez les journaux de votre serveur, vous verrez que la requête /hello a été enregistrée comme les requêtes précédentes :

Output[::]:3333: got /hello request

Pour arrêter le serveur, appuyez sur CONTROL+C.

Dans cette section, vous avez mis à jour votre fonction de gestionnaire getHello pour lire un nom à partir des données de formulaire publiées sur la page, puis renvoyer ce nom à l'utilisateur.

À ce stade de votre programme, certaines choses peuvent mal tourner lors du traitement d'une demande, et vos utilisateurs ne seront pas avertis. Dans la section suivante, vous mettrez à jour vos fonctions de gestionnaire pour renvoyer les codes d'état et les en-têtes HTTP.

Répondre avec des en-têtes et un code d'état

Le protocole HTTP utilise quelques fonctionnalités que les utilisateurs ne voient normalement pas pour envoyer des données afin d'aider les navigateurs ou les serveurs à communiquer. L'une de ces fonctionnalités s'appelle le code d'état [2] et est utilisée par le serveur pour donner à un client HTTP une meilleure idée de savoir si le serveur considère que la requête a réussi ou si quelque chose s'est mal passé côté serveur, ou avec la demande envoyée par le client.

Une autre façon de communiquer entre les serveurs et les clients HTTP consiste à utiliser les champs d'en-tête [3]. Un champ d'en-tête est une clé et une valeur qu'un client ou un serveur enverra à l'autre pour les informer d'eux-mêmes. Il existe de nombreux en-têtes prédéfinis par le protocole HTTP, tels que Accept, qu'un client utilise pour indiquer au serveur le type de données qu'il peut accepter et comprendre. Il est également possible de définir les vôtres en les préfixant avec x- puis le reste d'un nom.

Dans cette section, vous allez mettre à jour votre programme pour faire du champ de formulaire myName de getHello, un champ obligatoire. Si une valeur n'est pas envoyée pour le champ myName, votre serveur renverra un code d'état "Bad Request" au client et ajoutera un en-tête x-missing-field pour faire savoir au client quel champ était disparu.

Pour ajouter cette fonctionnalité à votre programme, ouvrez une dernière fois votre fichier main.go et ajoutez le contrôle de validation à la fonction gestionnaire getHello :

main.go

...

func getHello(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    fmt.Printf("%s: got /hello request\n", ctx.Value(keyServerAddr))

    myName := r.PostFormValue("myName")
    if myName == "" {
        w.Header().Set("x-missing-field", "myName")
        w.WriteHeader(http.StatusBadRequest)
        return
    }
    io.WriteString(w, fmt.Sprintf("Hello, %s!\n", myName))
}

...

Dans cette mise à jour, lorsque myName est une chaîne vide, au lieu de définir un nom par défaut de HTTP, vous envoyez plutôt un message d'erreur au client. Tout d'abord, vous utilisez la méthode w.Header().Set pour définir un en-tête x-missing-field avec une valeur de myName dans les en-têtes HTTP de réponse. Ensuite, vous utilisez la méthode w.WriteHeader pour écrire tous les en-têtes de réponse, ainsi qu'un code d'état "Bad Request", au client. Enfin, il reviendra hors de la fonction de gestionnaire. Vous voulez vous assurer de le faire afin de ne pas écrire accidentellement une réponse Hello, ! au client en plus des informations d'erreur.

Il est également important de s'assurer que vous définissez les en-têtes et que vous envoyez le code d'état dans le bon ordre. Dans une requête ou une réponse HTTP, tous les en-têtes doivent être envoyés avant que le corps ne soit envoyé au client. Par conséquent, toute demande de mise à jour de w.Header() doit être effectuée avant l'appel de w.WriteHeader. Une fois que w.WriteHeader est appelé, le statut de la page est envoyé avec tous les en-têtes et seul le corps peut être écrit après.

Une fois que vous avez enregistré vos mises à jour, vous pouvez relancer votre programme avec la commande go run :

go run main.go

Maintenant, utilisez votre deuxième terminal pour faire une autre requête curl -X POST à l'URL /hello, mais n'incluez pas le -F pour envoyer les données du formulaire. Vous voudrez également inclure l'option -v pour indiquer à curl d'afficher une sortie détaillée afin que vous puissiez voir tous les en-têtes et la sortie de la requête :

curl -v -X POST 'http://localhost:3333/hello'

Cette fois, dans la sortie, vous verrez beaucoup plus d'informations au fur et à mesure que la demande est traitée en raison de la sortie détaillée :

Output*   Trying ::1:3333...
* Connected to localhost (::1) port 3333 (#0)
> POST /hello HTTP/1.1
> Host: localhost:3333
> User-Agent: curl/7.77.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 400 Bad Request
< X-Missing-Field: myName
< Date: Wed, 02 Mar 2022 03:51:54 GMT
< Content-Length: 0
< 
* Connection #0 to host localhost left intact

Les deux premières lignes de la sortie indiquent que curl essaie de se connecter au port localhost 3333.

Ensuite, les lignes commençant par > indiquent la requête que curl adresse au serveur. Il indique que curl fait une requête POST à l'URL /hello en utilisant le protocole HTTP 1.1, ainsi que quelques autres en-têtes. Ensuite, il envoie un corps vide comme indiqué par la ligne vide >.

Une fois que curl a envoyé la requête, vous pouvez voir la réponse qu'il reçoit du serveur avec le préfixe <. La première ligne indique que le serveur a répondu avec un Bad Request, également connu sous le nom de code d'état 400. Ensuite, vous pouvez voir que l'en-tête X-Missing-Field que vous avez défini est inclus avec une valeur de myName. Après avoir envoyé quelques en-têtes supplémentaires, la requête se termine sans envoyer aucun corps, ce qui peut être vu par Content-Length (ou corps) ayant la longueur 0.

Si vous regardez à nouveau la sortie de votre serveur, vous verrez la requête /hello que le serveur a traitée dans la sortie :

Output[::]:3333: got /hello request

Une dernière fois, appuyez sur CONTROL+C pour arrêter votre serveur.

Dans cette section, vous avez mis à jour votre serveur HTTP pour ajouter une validation à l'entrée de formulaire /hello. Si un nom n'est pas envoyé dans le cadre du formulaire, vous avez utilisé w.Header().Set pour définir un en-tête à renvoyer au client. Une fois l'en-tête défini, vous avez utilisé w.WriteHeader pour écrire les en-têtes au client, ainsi qu'un code d'état indiquant au client qu'il s'agissait d'une mauvaise demande.

Conclusion

Dans ce didacticiel, vous avez créé un nouveau serveur HTTP Go à l'aide du package net/http dans la bibliothèque standard de Go. Vous avez ensuite mis à jour votre programme pour utiliser un multiplexeur de serveur spécifique et plusieurs instances http.Server. Vous avez également mis à jour votre serveur pour lire les entrées utilisateur via les valeurs de chaîne de requête, le corps de la requête et les données du formulaire. Enfin, vous avez mis à jour votre serveur pour renvoyer les informations de validation du formulaire au client à l'aide d'un en-tête HTTP personnalisé et d'un code d'état "Bad Request".

Une bonne chose à propos de l'écosystème Go HTTP est que de nombreux frameworks sont conçus pour s'intégrer parfaitement dans le package net/http de Go au lieu de réinventer une grande partie du code qui existe déjà. Le projet github.com/go-chi/chi en est un bon exemple. Le multiplexeur de serveur intégré à Go est un bon moyen de démarrer avec un serveur HTTP, mais il manque de nombreuses fonctionnalités avancées dont un serveur Web plus volumineux pourrait avoir besoin. Des projets tels que chi sont capables d'implémenter l'interface http.Handler dans la bibliothèque standard Go pour s'adapter parfaitement à la norme http.Server sans avoir besoin de réécrire la partie serveur du code. Cela leur permet de se concentrer sur la création de middleware et d'autres outils pour améliorer ce qui est disponible au lieu de travailler sur les fonctionnalités de base.

Outre des projets tels que chi, le package Go net/http inclut également de nombreuses fonctionnalités non couvertes dans ce didacticiel. Pour en savoir plus sur l'utilisation des cookies ou la gestion du trafic HTTPS, le package net/http est un bon point de départ.

Ce tutoriel fait également partie de la série DigitalOcean Comment coder dans Go. La série couvre un certain nombre de sujets Go, de l'installation de Go pour la première fois à l'utilisation du langage lui-même.