Ch’feir share – À la découverte du Go | Part 3


La team SFEIR Lille s’est réunie il y a peu pour une soirée dédiée au langage Go. Le programme était chargé : prise en main du langage Go autour d’un microservice REST JSON, adossé à un MongoDB et déployé dans Docker. Tout un programme et beaucoup de gros mots à la mode. Nous avons décidé de vous partager l’expérience acquise pendant cette soirée à travers trois articles, pour rendre la chose plus digeste. 

Si vous les avez ratés, nous vous conseillons de lire :

Ch’feir share – À la découverte du Go | Part 1 – Les bases

Ch’feir share – À la découverte du Go | Part 2 – Concurrence, channel et Go Routine

Voici la troisième et dernière partie de notre dossier consacré à ce langage.

Serveur Web

Dans notre dernière partie, nous allons voir comment servir en HTTP notre API REST. Puis nous mettrons tout bout à bout pour lancer notre première API REST JSON en Go. Un des packages les plus soignés de Go est le package HTTP. Du serveur aux tests, tout est prévu pour faciliter le développement de composants web. Pour nous faciliter le travail, nous utiliserons une bibliothèque de gestion de middleware Urfave Negroni (connu jusqu’à peu de temps sous le nom de codegangsta), ainsi qu’un multiplexeur de requêtes HTTP Gorilla Mux. Petit rappel sur la notion de serveur web, middleware et routeur avec le schéma ci-dessous :

Avant de commencer l'initiation au langage Go, Petit rappel sur la notion de serveur web, middleware et routeur

Le middleware permet d’intercepter toutes les requêtes faites à votre application pour y effectuer un traitement adéquat (logging, authentification, etc.) Le routeur est un middleware spécifique qui permet de rediriger une route spécifique d’une adresse HTTP vers une fonction de traitement dédiée. Pour illustrer notre propos, nous allons mettre en place un middleware de calcul de temps de réponse de l’application et personnaliser les middlewares par défaut livrés avec negroni. Enfin, nous allons associer différentes URL vers leurs handlers associés. Notre application va mettre en oeuvre les routes suivantes :

Routes définies avec le langage Go

La création du serveur web se déroule avec les étapes suivantes :

On instancie en premier les éléments nécessaires à la construction du serveur web : DAO, Negroni, handler et routeur.


// spirit dao
dao, err := dao.GetSpiritDAO(db, daoType)
if err != nil {
	logger.WithField("error", err).WithField("connection string", db).
                 Fatal("unable to  connect to mongo db")
}

// web server
n := negroni.New()

// new handler
handler := NewSpiritHandler(dao)

// new router
router := NewRouter(handler)

On ajoute ensuite tous les middlewares nécessaires au bon fonctionnement de notre serveur : logging logrus, recovery en cas de panic et statistique que nous verrons tout à l’heure. Negroni dispose de méthode pour instancier tous ces middlewares par défaut, mais nous prenons ici le temps de n’ajouter que ceux dont nous avons réellement l’utilité et nous adaptons leur configuration à nos besoins. Exemple, nous utilisons l’instance du logger de l’application au lieu d’en créer un second et nous évitons d’afficher la pile d’exception dans le réponse de notre serveur si une requête venait à lever une panic.


// add middleware for logging
n.Use(negronilogrus.NewMiddlewareFromLogger(logger.StandardLogger(), "spirit"))

// add recovery middleware in case of panic in handler func
recovery := negroni.NewRecovery()
recovery.PrintStack = false
n.Use(recovery)

// add statistics middleware
n.Use(NewStatisticsMiddleware(statisticsDuration))

En dernier on ajoutera le multiplexeur qui n’est autre qu’un middleware particulier.


// handler goes last 

 n.UseHandler(router.Mux)

On peut enfin lancer le serveur aux adresse et port de notre choix.


// serve 

 n.Run(":" + strconv.Itoa(port))

Comment créer son propre middleware ?

Un middleware n’est autre qu’une classe respectant l’interface suivante :


// Middleware interface 

 type Handler interface {

 ServeHTTP(rw http.ResponseWriter, r *http.Request, next   http.HandlerFunc) 

}

Elle prend en paramètre la requête, de quoi répondre à ladite requête et le prochain middleware à enchaîner. Dans le cas de notre middleware de statistiques, nous aurons quelques lignes à définir :


// StatisticsMiddleware is the middleware to record request statistics
type StatisticsMiddleware struct {
	Stat *utils.Statistics
}

// NewStatisticsMiddleware creates a new statistics middleware
func NewStatisticsMiddleware(duration time.Duration) *StatisticsMiddleware {
	return &StatisticsMiddleware{
		Stat: utils.NewStatistics(duration),
	}
}

func (sm *StatisticsMiddleware) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
	sm.Stat.PlusOne()
	next(rw, r)
}

Dernier point à étudier : le mapping route/handler que nous allons mettre dans une classe SpiritHandler.

On commence par définir la liste des routes et leur HandlerFunc associé dans le constructeur de notre SpiritHandler :


// NewSpiritHandler creates a new spirit handler to manage spirits
func NewSpiritHandler(spiritDAO dao.SpiritDAO) *SpiritHandler {
	handler := SpiritHandler{
		spiritDao: spiritDAO,
		Prefix:    "/spirits",
	}

	// build routes
	routes := []Route{}
	// GetAll
	routes = append(routes, Route{
		Name:        "Get all spirits",
		Method:      http.MethodGet,
		Pattern:     "",
		HandlerFunc: handler.GetAll,
	})
	// Get
	routes = append(routes, Route{
		Name:        "Get one spirit",
		Method:      http.MethodGet,
		Pattern:     "/{id}",
		HandlerFunc: handler.Get,
	})
           … 
}

Il nous faut alors écrire les HandlerFunc associées, comme la fonction Get :


// Get retrieve an entity by id
func (sh *SpiritHandler) Get(w http.ResponseWriter, r *http.Request) {
	// get the spirit ID from the URL
	spiritID := utils.ParamAsString("id", r)

	// find spirit
	spirit, err := sh.spiritDao.GetSpiritByID(spiritID)
	if err != nil {
		if err == mgo.ErrNotFound {
			logger.WithField("error", err).WithField("spirit ID", spiritID).
                                         Warn("unable to retrieve spirit by ID")
			utils.SendJSONNotFound(w)
			return
		}

		logger.WithField("error", err).WithField("spirit ID", spiritID).
                               Warn("unable to retrieve spirit by ID")
		utils.SendJSONError(w, "Error while retrieving spirit by ID",         
                               http.StatusInternalServerError)
		return
	}

	logger.WithField("spirits", spirit).Debug("spirit found")
	utils.SendJSONOk(w, spirit)
}

Cette fonction s’appuie sur notre DAO Mongo et sur quelques méthodes utilitaires pour récupérer les objets en base et les renvoyer au format JSON. Ci-dessous la méthode générique d’envoi d’objet au format JSON au client :


// SendJSONWithHTTPCode outputs JSON with an HTTP code
func SendJSONWithHTTPCode(w http.ResponseWriter, d interface{}, code int) {
	w.Header().Set("Content-Type", "application/json; charset=UTF-8")
	w.WriteHeader(code)
	if d != nil {
		err := json.NewEncoder(w).Encode(d)
		if err != nil {
			logger.WithField("body", d).WithField("code", code).
                                              Error("error while encoding JSON body of response")
			// panic will cause the InternalServerError to be send to users
			panic(err)
		}
	}
}

On positionne l’en-tête du message comme étant un message JSON/UTF-8, on envoie le code de retour en premier, et si un objet est spécifié, il est encodé au format JSON et renvoyé via le http.ResponseWriter.

Pour finir, nous allons automatiser les tests de notre serveur web et de ses différentes routes. Plusieurs façons de tester sont possibles : unitairement au niveau des HandlerFunc ou plus globalement au niveau serveur web Negroni. C’est cette deuxième possibilité que nous allons retenir, car elle est plus complète et met également en oeuvre les middlewares.


func TestSpiritHandlerGetServer(t *testing.T) {

      ts := httptest.NewServer(BuildWebServer("", dao.DAOMock, 250*time.Millisecond))
      defer ts.Close()

       res, err := http.Get(ts.URL + "/spirits")
       if err != nil {
	       t.Error(err)
       }

       var resSpirit []model.Spirit
       err = json.NewDecoder(res.Body).Decode(&resSpirit)

       if err != nil {
       	t.Errorf("Unable to get JSON content %v", err)
       }

       res.Body.Close()

       if res.StatusCode != http.StatusOK {
       	t.Error("Wrong response code")
       }

       if resSpirit[0] != dao.MockedSpirit {
       	t.Error("Wrong response body")
       }
}

On crée ici un serveur web de test auquel nous passons notre Negroni initialisé de ses middlewares et routeurs. On crée ensuite une requête Get sur l’URL « /spirits » qui correspond à notre HandleFunc GetAll. On décode le résultat comme un tableau de model.Spirit qui doit contenir un seul élément MockedSpirit, car nous utilisons notre implémentation mockée de DAO. Nous avons à présent un microservice HTTP Go qui sert du JSON et dont on peut tester chaque brique et route indépendamment.

Docker

Il nous reste à effectuer son déploiement dans Docker. Pour ce faire, on ne coupe pas à l’écriture d’un Dockerfile qui se veut très basique :


# minimal linux distribution
FROM golang:1.6-wheezy

# GO and PATH env variables already set in golang image

# set the go path to import the source project
WORKDIR $GOPATH/src/github.com/sebastienfr/handsongo
ADD . $GOPATH/src/github.com/sebastienfr/handsongo

# In one command-line (for reduce memory usage purposes),
# we install the required software,
# we build handsongo program
# we clean the system from all build dependencies
RUN make all &&; rm -rf $GOPATH/pkg && rm -rf $GOPATH/src

# by default, the exposed ports are 8020 (HTTP)
EXPOSE 8020

On repart de l’image Go 1.6 Debian Wheezy officielle, on compile et fait le ménage en une ligne et on expose le port 8020 pour y accéder de l’extérieur. Simple, efficace.

Pourquoi pas une image de base plus légère type Alpine ?

Parce que j’ai eu des problèmes de résolution de nom (DNS) avec docker-compose et Alpine il y a quelque temps et que je n’ai pas réessayé depuis.

Ensuite on lance tout ça avec une base MongoDB dans un docker-compose :


# handsongo micro-service
handsongo:
container_name: handsongo
image: sfeir/handsongo:latest
restart: always
links:
  - mongo
ports:
 - "8020:8020"
command: /go/bin/handsongo -port 8020 -logl debug -logf text -statd 15s -db mongodb://mongo/spirits

# bdd mockup
mongo:
container_name: handsongo-mongo
image: mongo:3.2
restart: always
ports:
 - "27017:27017"

Vous pouvez souffler

C’est terminé ! Avec ces trois articles, certes un peu touffus, vous avez les bases pour monter, étape par étape, un petit microservice du langage Go permettant d’exposer une API REST en JSON. Vous y trouverez les bonnes pratiques et les bibliothèques les plus utilisées pour mener à bien cette tâche.
N’hésitez pas à nous faire vos retours et à consulter le dépôt du projet, qui contient d’avantage de détails et va plus loin sur certains points, notamment les tests.

Vous aimerez aussi...