Go to Hackademy website

Créer une API simpliste en Go

Théo Delaune

Posté par dans la catégorie back

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.

Cette 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 table cars.
  • controllers/cars.go fait la relation entre les requêtes http et notre struct Car
  • models/car.go lie notre struct Car 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 var
func 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 readed
  defer 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 tout 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 types /cars/ vers /cars.

Pour définir une route nous utilisons plusieurs fonctions: -Methods() définit la méthode gérer 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 auquelle 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.


L’équipe Synbioz. Libres d’être ensemble.

Ajouter un commentaire