Le langage Go est rapidement devenu très populaire mais beaucoup hésitent encore à l'utiliser pour le développement de leurs nouvelles applications. Nous allons ici voir comment construire une api REST rapidement et facilement.
Introduction
La particularité du langage Go est sa simplicité d’écriture. La syntaxe est inspirée du langage C avec un code procédural. Il n’intègre pas de concept de classes mais fournit les mécanismes nécessaires à l’écriture de code dans un style orienté objet. Le code est concis et clair, principe KISS (Keep It Simple, Stupid). Nous allons donc voir comment utiliser ce language pour faire du web. Nous verrons dans un premier temps une implémentation du package "net/http" pour la création d'une api REST. Dans un second temps, je vous présenterai un utilitaire pour faciliter le développement d'une application web : Buffalo.
Package "http/net"
Documentation du package : https://golang.org/pkg/net/http/.
Pour commencer, nous allons créer un serveur http qui va écouter sur le port 8001
.
package main
import "net/http"
func main() {
//TO DO Implement handler
http.ListenAndServe(":8001", nil)
}
Pour démarrer le serveur, il suffit d’exécuter le fichier main.go
avec la commande :
go run main.go
Si vous essayez de faire une requête http sur 127.0.0.1:8001
, le serveur vous retournera une 404
puisque la route /
n'est pas spécifiée. Pour remédier à ce problème il faut implémenter un handler sur /
.
// Handle registers the handler for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
Pour cela, http.Handle a besoin d'un modèle qui va correspondre à la route de la requête et à un handler.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Un handler a besoin d’un objet de type ResponseWriter
et de la requête. Nous allons créer une méthode handler
. Ici, pour une api REST, la réponse doit être au format JSON. Nous allons donc ajouter au header le content-type JSON et retourner du contenu JSON. La méthode Write
de ResponseWriter
prend en paramètre un tableau de byte. Donc on ‘caste’ notre string contenant du JSON vers le format bytes avec la méthode byte[](string)
.
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
w.Write([]byte(`{"message": "Hello world !"}`))
}
Le code final de notre serveur donne donc :
package main
import "net/http"
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("content-type", "application/json")
w.Write([]byte(`{"message": "hello world"}`))
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8001", nil)
}
Cette fois-ci, si on lance le serveur et que l’on fait une requête http sur 127.0.0.1:8001
, le serveur répond bien un code 200 avec notre message en JSON.
Ce package est très bas niveau et assez pénible à utiliser. La communauté a donc mis à disposition différentes surcouches notamment au niveau du routing pour faciliter le développement.
Présentation de Buffalo
Buffalo est une librairie permettant de faciliter le développement avec Go. Elle utilise principalement les librairies de Gorilla https://github.com/gorilla
.
Pour installer buffalo lancer la commande :
go get -u -v github.com/gobuffalo/buffalo/buffalo
Une fois buffalo installé,
> $ buffalo help new
Buffalo version v0.9.0
Creates a new Buffalo application
Usage:
buffalo new [name] [flags]
Flags:
--api skip all front-end code and configure for an API server
--ci-provider string specify the type of ci file you would like buffalo to generate [none, travis, gitlab-ci] (default \"none\")
--db-type string specify the type of database you want to use [postgres, mysql, sqlite3] (default \"postgres\")
--docker string specify the type of Docker file to generate [none, multi, standard] (default \"multi\")
-f, --force delete and remake if the app already exists
-h, --help help for new
--skip-dep skips adding github.com/golang/dep to your app
--skip-pop skips adding pop/soda to your app
--skip-webpack skips adding Webpack to your app
-v, --verbose verbosely print out the go get/install commands
--with-yarn allows the use of yarn instead of npm as dependency manager
La commande new
permet de générer un nouveau projet. On va donc créer un projet api sans la base de données qui est gérée par pop
. Nous allons donc lancer cette commande pour générer la base de notre api REST. Placez-vous dans votre répertoire de travail ($GOPATH/src/your_user_name
par exemple) et lancez la commande suivante :
buffalo new api --api --skip-pop
Cette commande a créé le dossier api
. Celui-ci comprend :
- le fichier
main.go
, il s’agit de l’entrée de l'application ;
- le dossier
actions/
il s’agit du dossier contenant nos handlers ;
- le dossier
grifts/
il s’agit du dossier contenant les commandes
Le reste des fichiers ne nous intéresse pas.
Lancez le serveur :
buffalo dev
Ceci va compiler votre projet et démarrer le serveur.
package main
import (
"log"
"qneyrat/api/actions"
"github.com/gobuffalo/envy"
)
func main() {
port := envy.Get("PORT", "3000")
app := actions.App()
log.Fatal(app.Start(port))
}
Nous allons regarder ce que fait la méthode app.start()
.
// ...
server := http.Server{
Addr: fmt.Sprintf(":%s", addr),
Handler: a,
}
// ...
err := server.ListenAndServe()
Comme dans la première partie, Buffalo démarre le serveur http du package "http/net".
Lancez une requête sur 127.0.0.1:3000
, celle-ci nous retourne bien une réponse JSON. Allons maintenant voir dans actions.App()
ce qu’il se passe.
func App() *buffalo.App {
if app == nil {
//...
app.GET("/", HomeHandler)
}
return app
}
La function App()
va attacher à l'instance de *buffalo.App
les handlers de notre api. Ici, un handler est attaché à la route /
. Le handler HomeHandler
est du type Handler
.
type Handler func(Context) error
Handler prend en paramètre un Context
.
// Context holds on to information as you
// pass it down through middleware, Handlers,
// templates, etc... It strives to make your
// life a happier one.
type Context interface {
context.Context
Response() http.ResponseWriter
Request() *http.Request
Session() *Session
Params() ParamValues
Param(string) string
Set(string, interface{})
LogField(string, interface{})
LogFields(map[string]interface{})
Logger() Logger
Bind(interface{}) error
Render(int, render.Renderer) error
Error(int, error) error
Websocket() (*websocket.Conn, error)
Redirect(int, string, ...interface{}) error
Data() map[string]interface{}
Flash() *Flash
}
L'interface Context
va contenir le http.ResponseWriter
et *http.Request
comme dans l'exemple de la première partie. On peut remarquer que Context
dispose de plein d'autres interfaces qui permettront de faciliter le développement de notre handler.
Par exemple, pour retourner notre message JSON, on utilise Render
.
return c.Render(200, r.JSON(map[string]string{"message": "Welcome to Buffalo!"}))
Vous pouvez maintenant construire l'application et lancer le serveur. Buffalo offre un mode dev
qui va automatiquement recompiler votre application lorsque vous faites une modification dans le code. Pour cela, lancez la commande :
buffalo dev
Maintenant, si vous essayez de faire une requête sur 127.0.0.1:3000
, vous aurez bien votre message Welcome to Buffalo!
en JSON.
Pour faciliter le développement buffalo intègre le package grifts
qui permet la création de commandes. Les commandes sont déclarées dans le dossier grifts
.
buffalo task list
Par défaut, il existe la commande routes
qui permet de voir l'ensemble des routes et les handlers. Pour lancer cette commande, on va d'abord construire l'application pour contruire les routes puis jouer la tâche routes
.
buffalo build
buffalo task routes
Maintenant que buffalo a été présenté, nous allons créer de nouvelles routes. Vous pouvez retrouver l'ensemble du code sur mon github https://github.com/qneyrat/api.
Nous allons gérer une nouvelle ressource pour notre api, la ressource user
.
Créez le dossier models
et dedans le fichier user.go
. On va déclarer une structure User
composé d'un ID
.
package models
import (
"github.com/satori/go.uuid"
)
type User struct {
ID uuid.UUID `json:"id"`
}
Créez une nouvelle action dans le dossier actions
pour gérer la ressource user
. Créez donc un fichier users.go
.
Pour s'abstraire d'une base de données, on va créer une map pour stocker nos utilisateurs.
var db = make(map[uuid.UUID]models.User)
On va donc créer une fonction pour retourner dans un JSON l'ensemble des utilisateurs stockés dans "base de données" db
. Pour regrouper l'ensemble des handlers qui vont gérer notre ressource user
, on va créer une structure vide pour y attacher nos fonctions.
type UserResource struct{}
func (ur UserResource) List(c buffalo.Context) error {
return c.Render(200, r.JSON(db))
}
On va maintenant attacher ce nouvel handler à une route dans le fichier app.go
. Avant cela on va faire préfixer nos routes par /api/v1
.
// on préfixe nos nouvelles routes par /api/v1
g := app.Group("/api/v1")
// on déclare notre UserResource
ur := &UserResource{}
// on déclare notre route get qu'on rattache au handler UserResource.List
// le path est donc /api/v1/users
g.GET("/users", ur.List)
Si vous faites donc un GET
sur /api/v1/users
, l'api vous retourne une collection vide puisqu'il n'y a pas encore d’utilisateur.
On va donc créer un nouveau handler qui va en créer un.
// Create User.
func (ur UserResource) Create(c buffalo.Context) error {
// on crée un nouvel utilisateur
user := &models.User{
// on génère un nouvel id
ID: uuid.NewV4(),
}
// on l'ajoute à notre base de données
db[user.ID] = *user
return c.Render(201, r.JSON(user))
}
On ajoute la route dans app.go
.
g.POST("/users", ur.Create)
Maintenant, si vous faites un POST
sur /api/v1/users
, l'api vous retournera une 201 et vous informera que l’utilisateur a bien été créé. On va donc vérifier dans notre liste d’utilisateurs. Donc on fait un GET
sur /api/v1/users
et on constate qu’on a bien notre utilisateur dans la collection.
Pour finir, nous allons maintenant faire un handler pour afficher un utilisateur spécifique. On va donc créer un handler sur la route /users/{id}
.
func (ur UserResource) Show(c buffalo.Context) error {
// on récupère l'id de la requête qu'on formate en uuid
id, err := uuid.FromString(c.Param("id"))
if err != nil {
// si l'id n'est pas un uuid on génère une erreur
return c.Render(500, r.String("id is not uuid v4"))
}
// on récupère notre utilisateur dans notre base de données
user, ok := db[id]
if ok {
// si il existe, on le retourne
return c.Render(200, r.JSON(user))
}
// si il n'existe pas, on retourne une erreur 404
return c.Render(404, r.String("user not found"))
}
On attache ce nouveau handler à notre app
.
g.GET("/users/{id}", ur.Show)
Maintenant, on crée un utilisateur avec un POST
sur /api/v1/users
puis on fait un GET
sur /api/v1/users/{id}
en remplaçant {id}
par l'uuid de l’utilisateur que vous venez de créer. L'api vous retourne un code 200 avec les informations de l’utilisateur.
Vous avez maintenant une base d'api performante avec des outils pour développer rapidement et facilement une api en Go. Vous pouvez retrouver l'ensemble de la documentation et découvrir les autres fonctionnalités de buffalo sur http://gobuffalo.io.