Écrit par Théo Delaune
Go commence actuellement à avoir la côte auprès de nombreux développeurs, il devient de plus en plus utilisé dans la création d’application back-end.
Ces derniers mois, beaucoup de développeurs migrent leurs APIs Ruby on Rails en Go. Aujourd’hui nous allons nous y intéresser également et découvrir ensemble comment mettre en place une API simple en Go.
Cet article n’a pas vocation à vous apprendre les bases du Go. Pour cela vous pouvez retrouver nos précédents articles sur les débuts en Go.
Notre application
Nous allons réaliser la même API que celle réalisée dans un précédent article API Ruby on Rails Grape.
Notre but ici est de concevoir cette API avec les packages de base de Go. Les seuls packages externes sont: – gorilla/mux
qui permet de créer un routeur plus facilement que celui inclus dans les packages de Go. – lib/pq
qui est le driver PostgreSQL
Architecture
L’architecture de notre application se décline comme ceci:
config/ database.go controllers/ cars.go models/ car.go main.go router.go
config/database.go
permet la connexion à la base de données ainsi que la création de la tablecars
.controllers/cars.go
fait la relation entre les requêtes http et notre structCar
models/car.go
lie notre structCar
avec les actions en base de données.main.go
lance la connexion à notre base de données, ajoute un enregistrement à notre base de donnée et lance le serveur http.router.go
définit les routes de notre API
Création de notre fichier config/database.go
Ce fichier va nous permettre de pouvoir gérer la connexion à notre base de données, de partager cette instance de connexion et d’initialiser une table sur cette base de données.
Un pré-requis est obligatoire pour que tout fonctionne correctement, la base de donnée doit déjà être créée manuellement avant de s’y connecter avec notre API.
// Dans un terminal // Création d'une base de donnée sur PostgreSQL createdb cars -u username -h host
package config import ( "database/sql" _ "github.com/lib/pq" "log" ) var db *sql.DB func DatabaseInit() { var err error db, err = sql.Open("postgres", "user=theodelaune dbname=goapi") if err !=nil { log.Fatal(err) } // Create Table cars if not exists createCarsTable() } func createCarsTable() { _, err := db.Exec("CREATE TABLE IF NOT EXISTS cars(id serial,manufacturer varchar(20), design varchar(20), style varchar(20), doors int, created_at timestamp default NULL, updated_at timestamp default NULL, constraint pk primary key(id))") if err !=nil { log.Fatal(err) } } // Getter for db varfunc Db() *sql.DB { return db }
Nous définissons tout notre code contenant la gestion de la base de données dans le package config, dans le but de séparer le code de gestion de la base de donnée de celui responsable du lancement du serveur.
Une importation du type _ "..."
importe le package uniquement, elle évite le lancement de l’initialisation de ce package, donc de possibles effets de bords.
La variable db
va contenir notre instance de connexion SQL à la base de données, pour rendre cette connexion disponible aux autres packages, nous la définissons comme un Getter
public avec func Db()
.
Création de notre fichier models/car.go
Ce fichier fait la liaison entre notre contrôleur et la base de données.
Nous allons lui définir en premier lieu la struct Car
qui va contenir toutes les informations d’une voiture.
Nous mappons sur chaque champ du struct
sa représentation en json, sous la forme json:"field..."
Puis nous définissons un type Cars
qui est une Slice
(tableau plus flexible) de Car
, qui va permettre de contenir un ensemble de voitures.
package models import ( "github.com/synbioz/go_api/config" "log" "time" ) type Car struct { Id int `json:"id"` Manufacturer string `json:"manufacturer"` Design string `json:"design"` Style string `json:"style"` Doors uint8 `json:"doors"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type Cars []Car
Nous avons maintenant la structure de base, il nous faut maintenant créer une fonction permettant de stocker en base de donnée notre ‘objet’ Car
.
Pour ce faire nous allons créer une nouvelle fonction NewCar(c *Car)
qui prend comme paramètre un pointeur de type Car
.
config.Db()
est l’instance de la connexion à la base de données issue du fichier config/database.go
.
Cette fonction nous permet de persister notre variable c
de type Car
dans la base de données.
Scan()
nous permet de récupérer le retour de la requête SQL et l’affecter sur un champ de notre voiture c
.
func NewCar(c *Car) { if c ==nil { log.Fatal(c) } c.CreatedAt = time.Now() c.UpdatedAt = time.Now() err := config.Db().QueryRow("INSERT INTO cars (manufacturer, design, style, doors, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6) RETURNING id;", c.Manufacturer, c.Design, c.Style, c.Doors, c.CreatedAt, c.UpdatedAt).Scan(&c.Id) if err !=nil { log.Fatal(err) } }
Nous avons besoin également d’une fonction nous permettant de récupérer depuis la base de données un enregistrement par son id
.
Comme précédemment nous exécutons une requête sur la base de données et insérons le résultat dans les champs d’une variable de type Car
puis nous retournons l’adresse du pointeur de cette variable.
func FindCarById(id int) *Car { var car Car row := config.Db().QueryRow("SELECT * FROM cars WHERE id = $1;", id) err := row.Scan(&car.Id, &car.Manufacturer, &car.Design, &car.Style, &car.Doors, &car.CreatedAt, &car.UpdatedAt) if err !=nil { log.Fatal(err) } return&car }
Pour obtenir toutes les voitures depuis la base de données nous exécutons notre requête SQL qui n’est rien d’autre qu’un SELECT *
.
Nous itérons sur le retour de la requête, pour récupérer chaque ligne retournée.
Comme vu juste avant, nous récupérons les valeurs retournées de chaque ligne grâce à Scan()
et ajoutons chaque voiture récupérée dans la variable de type Cars
.
func AllCars() *Cars { var cars Cars rows, err := config.Db().Query("SELECT * FROM cars") if err !=nil { log.Fatal(err) } // Close rows after all readeddefer rows.Close() for rows.Next() { var c Car err := rows.Scan(&c.Id, &c.Manufacturer, &c.Design, &c.Style, &c.Doors, &c.CreatedAt, &c.UpdatedAt) if err !=nil { log.Fatal(err) } cars = append(cars, c) } return&cars }
La mise à jour d’un enregistrement est réalisée en pur SQL. Nous utilisons Prepare()
pour créer la requête et Exec()
pour la lancer.
func UpdateCar(car *Car) { car.UpdatedAt = time.Now() stmt, err := config.Db().Prepare("UPDATE cars SET manufacturer=$1, design=$2, style=$3, doors=$4, updated_at=$5 WHERE id=$6;") if err !=nil { log.Fatal(err) } _, err = stmt.Exec(car.Manufacturer, car.Design, car.Style, car.Doors, car.UpdatedAt, car.id) if err !=nil { log.Fatal(err) } }
La suppression d’un enregistrement en base est très simple à mettre en pratique :
func DeleteCarById(id int) error { stmt, err := config.Db().Prepare("DELETE FROM cars WHERE id=$1;") if err !=nil { log.Fatal(err) } _, err = stmt.Exec(id) return err }
Création de notre fichier controllers/cars.go
Nous devons à présent gérer la relation entre les requêtes http et notre struct Car
. Nous créons pour cela des fonctions permettant de gérer ses requêtes.
La fonction CarsIndex va nous permettre d’envoyer toutes les voitures présentes dans notre base de données.
Nous mettons en place des headers spécifiques, car nous souhaitons envoyer seulement du json
et répondre avec un status 200
.
Nous précisons par le biais de json.NewEncoder(w)
que la réponse est encodée au format json
, puis nous passons les données à encoder avec la fonction Encode()
.
Comme nous l’avons spécifié dans notre fichier models/car.go
, la fonction models.AllCars()
nous retourne une variable de type Cars
(qui est un Slice de Car).
package controllers import ( "encoding/json" "github.com/gorilla/mux" "github.com/synbioz/go_api/models" "io/ioutil" "log" "net/http" "strconv" ) func CarsIndex(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-type", "application/json;charset=UTF-8") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(models.AllCars()) }
Pour la création d’une nouvelle voiture, nous devons récupérer l’objet json
passé dans le corps de notre requête.
Nous devons la récupérer à partir du champ Body
et nous l’assignons à la variable body
.
json.Unmarshal()
parse l’objet json
contenu dans la variable body
et l’affecte à la variable car
.
Il ne reste plus qu’à persister cette variable grâce à notre fonction models.NewCar()
.
Nous renvoyons à l’utilisateur sa voiture qui a entre-temps était persistée en base au format json
.
func CarsCreate(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-type", "application/json;charset=UTF-8") w.WriteHeader(http.StatusOK) body, err := ioutil.ReadAll(r.Body) if err !=nil { log.Fatal(err) } var car models.Car err = json.Unmarshal(body, &car) if err !=nil { log.Fatal(err) } models.NewCar(&car) json.NewEncoder(w).Encode(car) }
Pour obtenir une voiture spécifique depuis son id, nous récupérons l’id passé dans l’url de la requête (que nous définirons dans la suite de cet article).
Nous utilisons pour se faire Vars()
du package gorilla/mux
, cette fonction nous permet de récupérer tous les paramètres passés dans l’url.
Pour convertir cet id au format string
en int
, nous utilisons le raccourci de ParseInt()
qui est Atoi()
issu du package strconv
.
func CarsShow(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-type", "application/json;charset=UTF-8") w.WriteHeader(http.StatusOK) vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err !=nil { log.Fatal(err) } car := models.FindCarById(id) json.NewEncoder(w).Encode(car) }
Dans le cas de la mise à jour et de la suppression d’un enregistrement nous ne repasserons pas dessus, car ces fonctions utilisent des composants vus dans la fonction CarShow()
et CarIndex()
.
func CarsUpdate(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-type", "application/json;charset=UTF-8") w.WriteHeader(http.StatusOK) vars := mux.Vars(r) id, err := strconv.Atoi(vars["id"]) if err !=nil { log.Fatal(err) } body, err := ioutil.ReadAll(r.Body) if err !=nil { log.Fatal(err) } car := models.FindCarById(id) err = json.Unmarshal(body, &car) models.UpdateCar(car) json.NewEncoder(w).Encode(car) } func CarsDelete(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-type", "application/json;charset=UTF-8") w.WriteHeader(http.StatusOK) vars := mux.Vars(r) // strconv.Atoi is shorthand for ParseInt id, err := strconv.Atoi(vars["id"]) if err !=nil { log.Fatal(err) } err = models.DeleteCarById(id) }
Création de notre fichier router.go
Dans ce fichier, nous allons gérer toutes les routes de notre application.
Le routeur de notre application est géré avec le routeur du package gorilla/mux
.
StrictSlash()
lorsqu’il est à true
redirige les routes du type /cars/
vers /cars
.
Pour définir une route, nous utilisons plusieurs fonctions : –Methods()
définit la méthode gérée par notre route (GET, POST, PUT, …). –Path()
correspond à la route à laquelle on veut faire correspondre notre action. –Name()
est le nom que l’on souhaite donner à notre route. –HandlerFunc()
spécifie la fonction à laquelle cette route est liée.
package main import ( "github.com/gorilla/mux" "github.com/synbioz/go_api/controllers" ) func InitializeRouter() *mux.Router { // StrictSlash is true => redirect /cars/ to /cars router := mux.NewRouter().StrictSlash(true) router.Methods("GET").Path("/cars").Name("Index").HandlerFunc(controllers.CarsIndex) router.Methods("POST").Path("/cars").Name("Create").HandlerFunc(controllers.CarsCreate) router.Methods("GET").Path("/cars/{id}").Name("Show").HandlerFunc(controllers.CarsShow) router.Methods("PUT").Path("/cars/{id}").Name("Update").HandlerFunc(controllers.CarsUpdate) router.Methods("DELETE").Path("/cars/{id}").Name("DELETE").HandlerFunc(controllers.CarsDelete) return router }
Création de notre fichier main.go
Ce fichier est le point central de notre application, il est responsable du lancement de la connexion à la base de données ainsi que du lancement du serveur http.
Nous lançons la fonction DatabaseInit()
écrite précédemment, cette fonction va créer la table cars
si elle n’existe pas et ouvrir une connexion à la base de données.
Nous initialisons par la suite les routes de notre application, nous permettant de définir quelles sont les routes pour lesquelles le serveur doit répondre.
Dans le cas de cet article, nous créons à chaque lancement du serveur une nouvelle voiture en base de données.
Puis, nous lançons le serveur avec http.ListenAndServe()
en lui spécifiant le port et le routeur utilisé.
package main import ( "github.com/synbioz/go_api/config" "github.com/synbioz/go_api/models" "log" "net/http" ) func main() { config.DatabaseInit() router := InitializeRouter() // Populate database models.NewCar(&models.Car{Manufacturer: "citroen", Design: "ds3", Style: "sport", Doors: 4}) log.Fatal(http.ListenAndServe(":8080", router)) }
Conclusion
Nous avons pu approcher avec cet article la création d’une API simpliste en Go.
Néanmoins, il reste beaucoup de choses qui ne sont pas encore gérées, comme la gestion des erreurs et de leur retour au client.
Je vous invite également à effectuer un refactoring du code, ainsi que de mettre en place un log sur le routeur qui permettra d’afficher dans la console les routes utilisées pour chaque appel.
Les sources de cet article sont disponibles sur GitHub.