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 import
s :
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.