af83

Écrire un service web en Go

Après avoir fait nos premiers pas avec Go, il est temps de passer à l'écriture d'un vrai programme. Je vous propose de faire un service web vraiment simple : il reçoit des requêtes HTTP et y répond un Hello world en JSON.

Commençons par créer un fichier hello.go. Tout fichier contenant du code Go doit indiquer de quel package il fait partie. Dans notre cas, nous allons utiliser le package main, qui est un package particulier : la fonction main définie dans ce package sera exécutée lorsque l'on lance le programme.

package main

Ensuite, nous importons deux packages provenant de la bibliothèque standard de Go : io et net/http. Le premier sert pour toutes les opérations d'entrées/sorties et le second pour faire des clients et des serveurs HTTP.

import (
    "io"
    "net/http"
)

Nous avons indiqué plus haut que la fonction main de notre package serait exécutée au lancement du programme… Il est temps de l'écrire. En go, la déclaration d'une fonction se fait avec le mot clé func et la syntaxe ressemble au C ou au JavaScript : les arguments sont listés entre parenthèses, et le corps de la fonction entre accolades.

func main() {
    // Le corps de la fonction
}

Dans notre cas, nous voulons faire deux choses dans la fonction main : démarrer un serveur HTTP et lui dire que l'on reçoit une requête - celle-ci devra être traitée par la fonction HelloWorld. Le module net/http nous fournit de quoi faire cela simplement en deux lignes :

func main() {
    http.HandleFunc("/", HelloWorld)
    http.ListenAndServe("127.0.0.1:8000", nil)
}

Il ne reste plus qu'à écrire la fonction HelloWorld. Pour le moment, nous allons renvoyer une chaîne de caractères en dur, la version évoluée sera pour plus tard. Les chaînes de caractères peuvent être délimitées par des doubles quotes ou par des backquotes. Nous allons préférer la seconde forme pour pouvoir mettre facilement des doubles quotes à l'intérieur :

`{"hello":"world"}`

Mais revenons à notre fonction HelloWorld. Pour être compatible avec le module net/http, elle doit prendre deux paramètres, le premier étant du type http.ResponseWriter et le second du type *http.Request (la petite étoile sert à indiquer que c'est un pointeur). Les personnes qui ont fait du C ou du Java risquent d'être surprises mais, en Go, le type vient toujours après le nom de la variable.

À l'intérieur de notre fonction, nous allons simplement utiliser io.WriteString pour écrire la réponse dans le http.ResponseWriter :

func HelloWorld(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, `{"hello":"world"}`)
}

Voilà, nous avons un programme fonctionnel. Il peut se lancer avec go run hello.go (ça compile le programme puis exécute la version compilée). Et on peut vérifier avec curl qu'il fonctionne :

% curl -v http://localhost:8000/
* About to connect() to localhost port 8000 (#0)
*   Trying 127.0.0.1... connected
* Connected to localhost (127.0.0.1) port 8000 (#0)
> GET / HTTP/1.1
> User-Agent: curl/7.21.6 (i686-pc-linux-gnu) libcurl/7.21.6 OpenSSL/1.0.0e zlib/1.2.3.4
> Host: localhost:8000
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sat, 07 Apr 2012 18:35:14 GMT
< Transfer-Encoding: chunked
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
* Closing connection #0
{"hello":"world"}

Maintenant que vous avez pu constater qu'il est très facile de créer un serveur HTTP en Go, nous allons voir comment l'améliorer et ainsi découvrir de nouvelles fonctionnalités de Go. Il pourrait par exemple être pratique de pouvoir spécifier l'interface et le port sur lequel notre serveur écoute.

Pour extraire des arguments de la ligne de commande, la bibliothèque standard de Go propose le module flag. Nous allons déclarer une variable addr de type string, puis utiliser le module flag pour la remplir à partir de la ligne de commande ou, à défaut, avec une valeur par défaut. Cela donne ces quelques lignes :

var addr string
flag.StringVar(&addr, "addr", "127.0.0.1:8000", "Bind to this address:port")
flag.Parse()
http.ListenAndServe(addr, nil)

Nous pouvons maintenant compiler notre programme et le lancer en lui passant l'option -addr. Au passage, notez que notre programme dispose aussi de l'option -h pour afficher l'aide :

% go build hello.go
% ./hello -h
Usage of ./hello:
  -addr="127.0.0.1:8000": Bind to this address:port
% ./hello -addr=:7000

Étape suivante : la gestion des erreurs. La fonction ListenAndServe peut échouer pour différentes raisons. Si on lance notre programme en simple utilisateur en lui demandant d'écouter sur un port inférieur à 1024, cet appel échouera parce que nous n'avons pas les droits suffisants.

En go, la gestion des erreurs se fait principalement en retournant une erreur ou nil quand tout va bien. Nous allons utiliser le module log (n'oubliez pas de l'ajouter à l'import) pour afficher l'erreur et mettre fin au programme :

err := http.ListenAndServe(addr, nil)
if err != nil {
    log.Fatal("ListenAndServe: ", err)
}

Cela donne cette sortie :

% ./hello -addr=:1
2012/04/07 20:51:21 ListenAndServe: listen tcp <nil>:1: permission denied

Il nous reste encore deux choses à voir : la première est de remplacer la chaîne de caractères en dur qui représente le JSON et la seconde de faire du routage dynamique sur les URL. Pour le JSON, la bibliothèque standard de Go répond une fois de plus à nos attentes en fournissant un module encoding/json.

Mais avant de l'utiliser, nous allons voir comment déclarer un nouveau type. Nous allons créer un type Response qui permettra d'avoir notre réponse sous forme structurée avant d'être encodée en JSON. Le mot-clé type prend d'abord le nom du nouveau type puis la définition du type. Cette définition peut être un type simple, comme int, ou un type composé. Dans notre cas, ce sera un struct avec un seul champ : Hello.

type Response struct {
    Hello string
}

Nous modifions maintenant notre fonction HelloWorld pour créer une variable de type Response puis l'encoder en JSON :

func HelloWorld(w http.ResponseWriter, req *http.Request) {
    var response Response
    response.Hello = "world"
    b, err := json.Marshal(response)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    io.WriteString(w, string(b))
}

Notons que les fonctions en Go peuvent renvoyer plusieurs éléments. C'est le cas de la fonction json.Marshall qui renvoie un tableau de bytes et une erreur. C'est très pratique car cela évite de devoir faire comme en C pour un retour qui peut à la fois être le résultat de la fonction et indiquer une erreur.

Quand une erreur se présente, nous la traitons en renvoyant une erreur 500 avec le message de l'erreur grâce à la fonction http.Error.

D'autre part, WriteString attend en deuxième paramètre une chaîne de caractères, pas un tableau de bytes. Nous faisons donc la conversion avec string(b).

Nous pouvons donc passer à la dernière évolution de notre programme : gérer des URL dynamiques. Plutôt que de répondre toujours Hello world, nous pourrions passer le nom en paramètre dans l'URL. Par exemple, un GET sur /hello/Bruno renverrait {"Hello":"Bruno"}.

Pour cela, nous utilisons un module externe, pat, écrit par Keith Rarick et Blake Mizerany (si ce nom vous est familier, c'est probablement car il est aussi l'auteur de Sinatra). Go permet d'utiliser très simplement des modules provenant de diverses sources. L'installation du module se fait d'un simple appel à go get github.com/bmizerany/pat. Ensuite, nous pouvons importer notre module en l'ajoutant à la liste des imports :

import (
    "encoding/json"
    "flag"
    "github.com/bmizerany/pat"
    "io"
    "log"
    "net/http"
)

Nous déclarons ensuite notre route dans la fonction main, à la place du http.HandleFunc :

m := pat.New()
m.Get("/hello/:name", http.HandlerFunc(HelloWorld))
http.Handle("/", m)

Et il ne reste plus qu'à récupérer le paramètre :name dans la fonction HelloWorld et à l'injecter dans notre variable response :

response.Hello = req.URL.Query().Get(":name")

Et voilà, nous pouvons maintenant tester le tout :

$ go run hello.go -addr=:7000
$ curl http://localhost:7000/hello/Bruno
{"Hello":"Bruno"}

J'espère que ce billet vous aura permis d'apprécier la simplicité de Go et vous aura donné envie d'aller plus loin avec. En attendant, vous pouvez consulter le code complet sur ce gist.

blog comments powered by Disqus