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


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 de 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. Voici sa première partie.

Les bases

Les prérequis pour cette présentation sont d’avoir un environnement “Unix friendly” avec Docker et un $GOPATH prêt à en découdre. L’introduction de Go est un bon point de départ, et le Go tour permet de ne pas être trop décontenancé pour la suite.

Après un petit rappel de Go, sa vie, son oeuvre et ses petits tocs, nous rentrons dans le vif du sujet avec la présentation de l’architecture cible du logiciel à venir :

Introduction de Go

Les étapes du soir vont reprendre le schéma du sol au plafond :

  1. étude de l’outillage (makefile, vendor)
  2. comment passer des arguments à notre programme, logger et gérer les dépendances
  3. le multi threading à la portée de tous (go routine et channel)
  4. je persiste mes données dans le temps (MongoDB)
  5. je sers mes données en JSON sur mon serveur HTTP
  6. je déploie le tout avec sa base de données sous docker

Le résultat final et les étapes par branche sont disponibles dans ce dépôt Git.

Make, Vendors & Co

Comment compile-t-on en Go ? The power of the Makefile ! Vous trouverez une très bonne base pour vous familiariser avec tout ça dans cet article. Comme vous pourrez constater avec notre Makefile maison, un premier appel à make vous donnera la liste des cibles disponible. Ensuite all qui appelle clean puis build et à la fin on test. Il est bon de ne pas oublier le go fmt (formatage / indentation du code) et un petit coup de lint (nomenclature, validation des structures conditionnelles, etc.) ne fait jamais de mal. Beaucoup de projets libres imposent le formatage, oui on utilise des espaces pour l’indentation, les accolades se mettent en fin de ligne et puis c’est tout. Bienvenue dans le monde bien cadré de Go.

Là où c’est un peu moins cadré pour le moment, ou du moins plus artisanal, c’est côté gestion des dépendances. Go génère des binaires autonomes des OS pour lesquels ils sont compilés. Pas d’édition de lien dynamique pour le code natif de Go, exit les DLL, etc. Ce qui implique que toutes les sources des dépendances sont nécessaires à la compilation et pas de distribution de librairie binaire en Go possible. Je vous laisse creuser cette problématique pour mieux la comprendre. Cela implique qu’il vous faudra tout le code source de toutes vos dépendances et des dépendances de celles-ci pour compiler votre code ! Tout ne peut pas être parfait… Selon les dernières statistiques BigQuery, 78% des projets Go de GitHub ont adopté le vendoring. Pour éviter les conflits de version, et rendre vos compilations reproductibles, une seule solution : la gestion de configuration de vos dépendances avec votre projet. C’est ici que les vendors entrent en scène. Le vendoring est arrivé avec la version de Go 1.5, de manière expérimentale grâce à l’incantation suivante :

export GO15VENDOREXPERIMENT=1

Beaucoup d’outils existent pour faire du vendoring. Nous utiliserons godep pour notre démonstration. Mais au fait, que font ces outils ?

vue d'ensemble de Go

Les outils de vendoring copient (et mettent à jour) les dépendances de votre programme présentes dans votre $GOPATH, dans un répertoire nommé vendor de votre projet. Ensuite,go build, grâce à notre incantation précédente, saura qu’il doit utiliser ce répertoire et non plus le $GOPATH en entier pour résoudre les dépendances. Concernant godep pour la sauvegarde de vos dépendances, voici la démarche à suivre en fin de développement :

go get github.com/tools/godep
export GO15VENDOREXPERIMENT=1
godep save ./...

Command line

Maintenant, tentons de démarrer un simple programme Go. La première chose que nous voudrons faire sera de passer une liste d’arguments à notre programme, un basique de beaucoup de langage étant le parsing des arguments. Go n’est pas en reste avec ses API natives :

import "flag"
...
var flagvar int
flag.Var(&flagVal, "name", "help message for flagname")
flag.Parse()
fmt.Println("flagvar has value ", flagvar)

Néanmoins, nos amis de chez codegangsta (ça ne s’invente pas), mettent à disposition un outil similaire un peu plus riche.

N.B. : codegangsta a décidé de changer de nom depuis quelques jours, codegansta est mort : longue vie à urfave !


import "github.com/codegangsta/cli"
var port = 8020
func main() {
            // new app
	app := cli.NewApp()
	app.Name = "handsongo"
	app.Usage = "handsongo service launcher"
            // command line flags
	app.Flags = []cli.Flag{
		cli.IntFlag{
			Value: port,
			Name:  "port",
			Usage: "Set the listening port of the webserver",
		},
           }
            // main action
	// sub action are possible also
	app.Action = func(c *cli.Context) {
		// parse parameters
		port = c.Int("port")
		fmt.Printf("port : %d\n", port)
	}
            // run the app
	err = app.Run(os.Args)
	if err != nil {
		logger.Fatalf("Run error %q\n", err)
	}
}

Logging

Go dispose de son API de logging donc, mais qui est assez limitée. Pas de niveau de log par exemple. Un logger souvent rencontré est Logrus. J’ai beaucoup vu l’utilisation d’une ou deux instances de logger (sortie standard et / ou sortie des erreurs) par programme. On est bien loin de la souplesse d’un Log4J avec logging par package, dont la configuration est chargée à chaud. Mais, il y a toujours un mais, ce n’est pas gênant car pour les logs aussi, Go n’a pas froid aux yeux. Tout sortir en niveau Debug ne pose pas particulièrement de problème, à condition d’y mettre les clés adéquates pour leur exploitation. Je vous renverrai aux réflexions de Dave Cheney concernant les niveaux de log, passionnant. Il en ressort qu’avec des niveaux à Debug, Info et Erreur, on peut tout faire. Et il se trouve qu’avec les outils modernes d’exploitation desdits logs, il n’a pas tort ! Nous concernant comment allons-nous instancier et configurer nos logs ? Et bien comme ceci :

import "github.com/Sirupsen/logrus"
// InitLog initializes the logrus logger
func InitLog(logLevel, formatter string) error {
	logrus.SetFormatter(&logrus.TextFormatter{
		ForceColors:   true,
		FullTimestamp: true,
	})
	logrus.SetOutput(os.Stdout)
	level, err := logrus.ParseLevel(logLevel)
	if err != nil {
		logrus.SetLevel(logrus.DebugLevel)
		return err
	}
	logrus.SetLevel(level)
	return nil
}

Logrus est thread safe et met à disposition une instance par défaut. Il nous suffit donc d’appeler les méthodes de configuration qui s’appliqueront sur cette instance. On a le choix du format de sortie des logs avec coloration, s’il vous plaît. On positionne la sortie du logger et son niveau. Ensuite nous pouvons l’utiliser n’importe où dans notre programme de la manière suivante :

import logger "github.com/Sirupsen/logrus"
…
 logger.WithField("error", err).Warn("mongo db connection")

On utilise ici un alias dans l’import, au lieu d’utiliser le nom du package logrus comme précédemment. On définit ici un alias logger. Ensuite on peut déclarer autant de champs à logger que souhaité avec WithField(« message », valeur) et en dernier le message de log avec son niveau. Voici ce que donne le parsing d’un niveau de log erroné pour notre programme :

WARN[2016-06-01T13:12:40+02:00] error setting log level, using debug as default  error=not a valid logrus Level: "bla"

Dès que vous aurez digéré cette première partie, vous pourrez enchainer avec :

Vous aimerez aussi...