La version 1.11 de Go apporte un nouveau concept: les modules. Ils permettent une gestion plus efficace des packages (librairies) et accessoirement (ou pas) annoncent la disparition du GOPATH.
Je vais dans ce billet vous présenter cette nouveauté avec un exemple concret.

Création d’un module

Une chose trés importante: on ne crée pas un module dans le $GOPTAH. Dans mon cas, j’ai créé un répertoire spécifique: ~/projects/go/modules qui va contenir mes modules.
Je vais y créer le répertoire calc qui va contenir notre module d’exemple:

$ cd ~/projects/go/modules
$ mkdir calc
$ cd calc

Notre module va exporter une fonction qui retourner la somme de deux entiers:

package calc

// Calcule retourne la somme x + y
func Calcule(x, y int) int {
   return x + y
}

Pour l’instant ce n’est qu’un simple package, pour le transformer en module, il faut exécuter la commande:

$ go mod init github.com/toorop/calc
go: creating new go.mod: module github.com/toorop/calc

Cette commande à créé dans le répertoire de notre paquet un fichier go.mod contenant:

module github.com/toorop/calc

Voila notre module est créé, il ne nous faut à présent le publier:

$ git init 
$ git add * 
$ git commit -am "First commit"
$ git push -u origin master

Notre module est publié, n’importe qui va pouvoir l’utiliser en utilisant un classique:

$ go get github.com/toorop/calc

Qui va récupérer le code de la branche master

Et là vous allez vous dire:

Hein ? Tout ça pour ça !! En fait un module est un paquet classique, c’était bien la peine ! 😡

Mais non rassurez vous c’est un peu plus que ça 😎

Gestion des versions

Ce un peu plus c’est que les modules, contrairement aux packages, sont versionnés ce qui va nous permettre d’avoir des reproductible builds (désolé pour cet anglicisme mais je ne trouve pas l’équivalent en Français). Avec des packages classiques et donc sans gestion de version nous prenons le risque que notre code ne soit plus compatible avec les packages qu’il utilise en cas de go get -u (je suis à peu prés certain que ça vous est déja arrivé non ? ). La gestion des versions des modules Go va résoudre ce probléme puisque l’on va définir la version que l’on souhaite utiliser via le fichier go.mod ainsi plus de conflit de versions en cas de mise à jour.

Pour versionner notre module on va utiliser la gestion sémantique de version (SEMVER pour les initiés). Pour résumer la version va être definie sous la forme X.Y.Z avec:

  • X représentant le numéro de version MAJEURE, un changement de valeur implique des modifications non rétrocompatibles.
  • Y est le numéro de version MINEURE, ce chiffre est incrémenté en cas de modifications rétrocompatibles. Le plus souvent quand on ajoute des fonctionnalités sans changer les fonctionnalités et signatures de celles qui existent.
  • Z est le numéro de version CORRECTIF, qui est lié aux corrections de bugs.

Pour définir la version de notre module on va utliser les tags git. A noter que par défault quand vous allez “go getter” un module, Go va récuperer la dernière version.

On va donc ajouter la version, ici 1.0.0, à notre module:

$ git tag v1.0.0
$ git push --tags

Il est plus que judicieux de créer une branche par version, ça va nous permettre de maintenir différentes versions et de libérer la branche master:

$ git checkout -b v1
$ git push -u origin v1

Utilisation d’un module

On va à présent passer de l’autre coté du mirroir en créant une application qui va utiliser notre module.
Voici le code de notre petit programme de test:

package main

import (
    "fmt"

    "github.com/toorop/calc"
)

func main() {
    fmt.Printf("4 + 2 = %d\n", calc.Calcule(4, 2))
}

On active la gestion des modules:

$ go mod init calculatrice
go: creating new go.mod: module calculatrice

On peut à présent compiler:

$ go build
go: finding github.com/toorop/calc v1.0.0
go: downloading github.com/toorop/calc v1.0.0

Comme indiqué plus haut Go va par défaut récupérer le module avec le numéro de version le plus élévé (oui je sais dans notre cas il n’y a, pour le moment, qu’une version mais faites comme si 😉 )

On voit que cette commande a ajouté un fichier go.sum qui contient les hashes des paquets, ce qui permet de nous assurer que les paquets utilisés ne sont pas corrompu:

$ cat go.sum
github.com/toorop/calc v1.0.0 h1:wnksVnXF8gb91V3BI1igV2pAE6+aTVr1A2YndpUArTo=
github.com/toorop/calc v1.0.0/go.mod h1:2lGkzKVrkalvqMQ1ivl87JaVmmgLkRS8qMSUbkhYXpM=

On peut tester le binaire généré:

$ ./calculatrice
4 + 2 = 6

Mise à jour MINEURE du module

Pris d’une illumination soudaine, on se dit que ce serait bien si notre module calc pouvait également faire des soustractions.

On va donc ajouter une fonction Soustrais :

package calc

// Calcule retourne la somme x + y
func Calcule(x, y int) int {
   return x + y
}

// Soustrais retourne la soustraction x - y
func Soustrais(x, y int) int {
    return x - y
}

Cette modification ne change pas le comportement de la version 1.0.0, c’est un ajout de fonctionnalité, on va donc uniquement incrémenter la MINEURE:

$ git commit -m "Ajout de la fonction Soustrais" calc.go
$ git tag v1.1.0
$ git push --tags origin v1

Revenons à notre programme, vu que l’on n’utilise pas la nouvelle fonctionnalité, on n’est pas obligé de faire la mise à jour de notre module. Mais pour autant rien ne nous empéche de le faire, cela s’averera particulierement utile en cas de correction de bug et dans tous les cas on ne risque pas de “casser” notre programme puisque la mise à jour ne se ferra que sur les version mineures ou les correctifs.

$ go get -u
go: finding github.com/toorop/calc v1.1.0
go: downloading github.com/toorop/calc v1.1.0

Notre go.mod à été mis à jour est contient:

$ cat go.mod
module calculatrice

require github.com/toorop/calc v1.1.0

Mise à jour MAJEURE du module

Avec l’ajout de la fonction Soustrais on se dit que ce serait bien de renommer la fonction Calcule en Additionne:

package calc

// Additionne retourne la somme x + y
func Additionne(x, y int) int {
    return x + y
}

// Soustrais retourne la soustraction x - y
func Soustrais(x, y int) int {
    return x - y
}

Cette modification va casser la rétrocompatibilité avec la version 1.Y.Z de notre module, il faut donc incrémenter la MAJEURE en créant un nouveau tag v2.0.0 et en indiquant ce changement dans notre fichier go.mod. Par la même occasion, même si ce n’est pas obligatoire, on crée une nouvelle branche v2:

$ git commit  -m "Renommage de la fonction Calcule en Additionne" calc.go
$ git checkout -b v2
$ echo "module github.com/toorop/calc/v2" > go.mod
$ git commit -m "Mise à jour vers la version 2.0.0" go.mod
$ git tag v2.0.0
$ git push --tags origin v2

Si on fait un go get -u au niveau de notre application, on constate que rien ne se passe, si on veut explicitement passer à la version 2 il faut modifier notre import en ajoutant à la fin de notre “uri” d’import la version: github.com/toorop/calc/v2. Petite précision cela ne veut pas dire que l’on devra utiliser v2.Additionne() pour faire un addition on continuera à utiliser le nom du module calc:

package main

import (
    "fmt"

    "github.com/toorop/calc/v2"
)

func main() {
    fmt.Printf("4 + 2 = %d\n", calc.Additionne(4, 2))
}

Lors du go build, la version 2 du module va être récupéré et le go.mod modifié:

$ cat go.mod
module calculatrice

require (
        github.com/toorop/calc v1.1.0
        github.com/toorop/calc/v2 v2.0.0
)

On remarque que le require github.com/toorop/calc v1.1.0 est toujours présent, c’est “normal”, lors d’un go build aucune dépendance n’est supprimée, il faut faire le ménage explicitement via un go mod tidy:

$ go mod tidy
$ cat go.mod
module calculatrice

require github.com/toorop/calc/v2 v2.0.0

Inclure les modules aux sources de votre application

Il est tout à fait possible d’inclure les sources des différent modules utilisés par vortre application dans l’arborescence de son code source. Pour cela il faut exécuter la commande:

$ go mod vendor

Qui va créer un répertoire vendor à la racine du projet:

$ tree vendor/
vendor/
├── github.com
│   └── toorop
│       └── calc
│           └── v2
│               ├── calc.go
│               └── go.mod
└── modules.txt

4 directories, 3 files

Attention cependant un go build ne va pas utiliser les dépendances présentent dans le répertoire vendor, pour que ce soit le cas il faut le préciser via l’option -mod vendor, autrement dit en exécutant la commande go build -mod vendor

Voila pour cette petite introduction aux modules Go, je reviendrais sur le sujet plus tard pour présenter des cotés plus pratique, comme par exemple comment configurer son éditeur pour que l’autocompletion fonctionne avec les modules, ou comment configurer un service d’intégration continue pour utiliser les modules Go, ou…

👍 Si ce billet vous a plu n’hésitez pas à le partager ⤵️