Go Modules

2019-04-15 12 Min. lecture Golang

1. Introduction

Go 1.11 et 1.12 incluent un support préliminaire des modules, le nouveau système de gestion des dépendances de Go qui rend explicite et facile à gérer les informations de version des dépendances. Ce billet de blog est une introduction aux opérations de base nécessaires pour commencer à utiliser les modules. Un prochain article couvrira la publication des modules pour d’autres puissent les utiliser.

Un module est une collection de paquets Go stockés dans une arborescence de fichiers avec un fichier go.mod à la racine. Le fichier go.mod définit le chemin du module, qui est aussi le chemin d’importation utilisé pour le répertoire racine, et ses exigences de dépendance, qui sont les autres modules nécessaires pour que la compilation réussisse. Chaque exigence de dépendance est écrite sous la forme d’un chemin de module et d’une version sémantique spécifique.

Depuis Go 1.11, la commande go permet l’utilisation de modules lorsque le répertoire courant ou tout répertoire parent possède un go.mod, à condition que le répertoire soit en dehors de $GOPATH/src (dans $GOPATH/src, par compatibilité, la commande go fonctionne toujours dans l’ancien mode GOPATH, même si un go.mod est trouvé. Voir la documentation de la commande go pour plus de détails.) A partir de Go 1.13, le mode module sera le mode par défaut pour tous les développements.

Ce post passe en revue une séquence d’opérations courantes qui surviennent lors du développement de code Go avec des modules :

  • Créer un nouveau module.
  • Ajout d’une dépendance.
  • Mise à jour des dépendances.
  • Ajout d’une dépendance sur une nouvelle version majeure.
  • Mettre à niveau une dépendance vers une nouvelle version majeure.
  • Suppression des dépendances inutilisées.

2. Création d’un nouveau module

Créons un nouveau module.

Créez un nouveau répertoire vide quelque part en dehors de $GOPATH/src, placez vous dans ce répertoire, puis créez un nouveau fichier source, hello.go :

package hello

func Hello() string {
    return "Hello, world."
}

Faisons un test, aussi, dans hello_test.go :

package hello

import "testing"

func TestHello(t *testing.T) {
    want := "Hello, world."
    if got := Hello(); got != want {
        t.Errorf("Hello() = %q, want %q", got, want)
    }
}

À ce stade, le répertoire contient un paquet, mais pas un module, car il n’y a pas de fichier go.mod. Si nous travaillions dans /home/gopher/hello et que nous faisions des tests maintenant, nous verrions:

$ go test
PASS
ok      _/home/gopher/hello    0.020s
$

La dernière ligne résume le test global du paquet. Parce que nous travaillons en dehors de $GOPATH et aussi en dehors de n’importe quel module, la commande go ne connaît aucun chemin d’importation pour le répertoire courant et en fait un faux basé sur le nom du répertoire : _/home/gopher/hello.

Faisons du répertoire courant la racine d’un module en utilisant go mod init, puis essayons à nouveau go test :

$ go mod init example.com/hello
go: creating new go.mod: module example.com/hello
$ go test
PASS
ok      example.com/hello    0.020s
$

Félicitations ! Vous avez écrit et testé votre premier module.

$ cat go.mod
module example.com/hello

go 1.12
$

Le fichier go.mod n’apparaît que dans la racine du module. Les paquets dans les sous-répertoires ont des chemins d’importation composés du chemin du module plus le chemin vers le sous-répertoire. Par exemple, si nous avons créé un sous-répertoire world, nous n’aurions pas besoin (ni ne voudrions) lancer go mod init là-bas. Le paquet serait automatiquement reconnu comme faisant partie du module example.com/hello, avec le chemin d’importation example.com/hello/world.

3. Ajout d’une dépendance

La motivation première des modules Go était d’améliorer l’expérience d’utilisation (c’est-à-dire d’ajouter une dépendance au code écrit par d’autres développeurs).

Mettons à jour notre hello.go pour importer rsc.io/quote et l’utiliser pour implémenter Hello :

package hello

import "rsc.io/quote"

func Hello() string {
    return quote.Hello()
}

Maintenant, recommençons le test :

$ go test
go: finding rsc.io/quote v1.5.2
go: downloading rsc.io/quote v1.5.2
go: extracting rsc.io/quote v1.5.2
go: finding rsc.io/sampler v1.3.0
go: finding golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: downloading rsc.io/sampler v1.3.0
go: extracting rsc.io/sampler v1.3.0
go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
go: extracting golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
PASS
ok      example.com/hello    0.023s
$

La commande go résout les importations en utilisant les versions spécifiques du module de dépendance listées dans go.mod. Lorsqu’elle rencontre une importation d’un paquet non fourni par un module dans go.mod, la commande go recherche automatiquement le module contenant ce paquet et l’ajoute à go.mod, en utilisant la dernière version. ("Dernière" étant défini comme la dernière version stable tagué (non pré-version), ou bien la dernière pré-version taguée, ou bien la dernière version non taguée). Dans notre exemple, go test a résolu la nouvelle importation rsc.io/quote dans le module rsc.io/quote v1.5.2. Il a également téléchargé deux dépendances utilisées par rsc.io/quote, à savoir rsc.io/sampler et golang.org/x/text. Seules les dépendances directes sont enregistrées dans le fichier go.mod :

$ cat go.mod
module example.com/hello

go 1.12

require rsc.io/quote v1.5.2
$

Une deuxième commande go test ne répétera pas ce travail, puisque le fichier go.mod est maintenant à jour et les modules téléchargés sont mis en cache localement (dans $GOPATH/pkg/mod) :

$ go test
PASS
ok      example.com/hello    0.020s
$

Notez que si la commande go rend l’ajout d’une nouvelle dépendance rapide et facile, ce n’est pas sans coût. Votre module dépend désormais de la nouvelle dépendance dans des domaines essentiels tels que la conformité, la sécurité et les licences appropriées, pour n’en citer que quelques-uns. Pour plus de détails, voir le billet du blog de Russ Cox, "Our Software Dependency Problem".

Comme nous l’avons vu plus haut, l’ajout d’une dépendance directe entraîne souvent d’autres dépendances indirectes. La commande go list -m all liste le module courant et toutes ses dépendances :

$ go list -m all
example.com/hello
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$

Dans le résultat go list, le module courant, également appelé module main, est toujours la première ligne, suivi des dépendances triées par chemin du module.

La version golang.org/x/text v0.0.0.0-2017091503232832-14c0d48ead0c est un exemple de pseudo-version, qui est la syntaxe de version de la commande go pour un commit non tagué donné.

En plus du fichier go.mod, la commande go maintient un fichier nommé go.sum contenant les hachages cryptographiques attendus du contenu des versions spécifiques des modules :

$ cat go.sum
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZO...
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:Nq...
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3...
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPX...
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/Q...
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9...
$

La commande go utilise le fichier go.sum pour s’assurer que les téléchargements futurs de ces modules récupèrent les mêmes bits que le premier téléchargement, afin que les modules dont dépend votre projet ne changent pas de manière inattendue, que ce soit pour des raisons malicieuses, accidentelles ou autres. go.mod et go.sum doivent tous deux être vérifiés dans le contrôle de version.

4. Mise à jour des dépendances

Avec les modules Go, les versions sont référencées par des tagues de version sémantiques. Une version sémantique comporte trois parties : majeure, mineure et patch. Par exemple, pour la v0.1.2, la version majeure est 0, la version mineure est 1 et la version patch est 2. Dans la prochaine section, nous envisagerons une mise à jour majeure de la version.

À partir de la sortie de de go list -m all, nous pouvons voir que nous utilisons une version non taguée de golang.org/x/text. Passons à la dernière version balisée et testons que tout fonctionne encore :

$ go get golang.org/x/text
go: finding golang.org/x/text v0.3.0
go: downloading golang.org/x/text v0.3.0
go: extracting golang.org/x/text v0.3.0
$ go test
PASS
ok      example.com/hello    0.013s
$

Woohoo ! Tout passe. Jetons un autre coup d’œil au résultat de go list -m all et au fichier go.mod :

$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/sampler v1.3.0
$ cat go.mod
module example.com/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote v1.5.2
)
$

Le paquet golang.org/x/text a été mis à jour vers la dernière version taguée (v0.3.0). Le fichier go.mod a été mis à jour pour spécifier la version 0.3.0. Le commentaire indirect indique qu’une dépendance n’est pas utilisée directement par ce module, mais seulement indirectement par d’autres dépendances du module. Voir les modules d’aide pour plus de détails.

Essayons maintenant de mettre à jour la version mineure de rsc.io/sampler. Commencez de la même façon, en lançant go get et en lançant des tests :

$ go get rsc.io/sampler
go: finding rsc.io/sampler v1.99.99
go: downloading rsc.io/sampler v1.99.99
go: extracting rsc.io/sampler v1.99.99
$ go test
--- FAIL: TestHello (0.00s)
    hello_test.go:8: Hello() = "99 bottles of beer on the wall, 99 bottles of beer, ...", want "Hello, world."
FAIL
exit status 1
FAIL    example.com/hello    0.014s
$

L’échec du test montre que la dernière version de rsc.io/sampler est incompatible avec notre application. Énumérons les versions balisées disponibles de ce module :

$ go list -m -versions rsc.io/sampler
rsc.io/sampler v1.0.0 v1.2.0 v1.2.1 v1.3.0 v1.3.1 v1.99.99
$

Nous avions utilisé la v1.3.0 ; la v1.99.99 n’est clairement pas bonne. Peut-être que nous pouvons essayer d’utiliser la v1.3.1 à la place :

$ go get rsc.io/sampler@v1.3.1
go: finding rsc.io/sampler v1.3.1
go: downloading rsc.io/sampler v1.3.1
go: extracting rsc.io/sampler v1.3.1
$ go test
PASS
ok      example.com/hello    0.022s
$

Notez le @v1.3.1 explicite dans l’argument de go get. En général, chaque argument passé à go get peut prendre une version explicite ; la valeur par défaut est @latest, qui se traduit par la dernière version tel que défini précédemment.

5. Ajout d’une dépendance sur une nouvelle version majeure

Ajoutons une nouvelle fonction à notre package : func Proverb retourne un proverbe Go concurrency, en appelant quote.Concurrency, qui est fourni par le module rsc.io/quote/v3. Tout d’abord, nous mettons à jour hello.go pour ajouter la nouvelle fonction :

package hello

import (
    "rsc.io/quote"
    quoteV3 "rsc.io/quote/v3"
)

func Hello() string {
    return quote.Hello()
}

func Proverb() string {
    return quoteV3.Concurrency()
}

Ensuite, nous ajoutons un test à hello_test.go :

func TestProverb(t *testing.T) {
    want := "Concurrency is not parallelism."
    if got := Proverb(); got != want {
        t.Errorf("Proverb() = %q, want %q", got, want)
    }
}

Ensuite, nous pourrons tester notre code :

$ go test
go: finding rsc.io/quote/v3 v3.1.0
go: downloading rsc.io/quote/v3 v3.1.0
go: extracting rsc.io/quote/v3 v3.1.0
PASS
ok      example.com/hello    0.024s
$

Notez que notre module dépend maintenant de rsc.io/quote et rsc.io/quote/v3 :

$ go list -m rsc.io/q...
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
$

Chaque version majeure différente (v1, v2, etc.) d’un module Go utilise un chemin de module différent : à partir de v2, le chemin doit se terminer dans la version majeure. Dans l’exemple, la v3 de rsc.io/quote n’est plus rsc.io/quote : elle est identifiée par le chemin du module rsc.io/quote/v3. Cette convention s’appelle l’importation sémantique de versions, et elle donne des noms différents aux paquets incompatibles (ceux avec des versions majeures différentes). En revanche, la version 1.6.0 de rsc.io/quote devrait être rétrocompatible avec la version 1.5.2, donc elle réutilise le nom rsc.io/quote. (Dans la section précédente, rsc.io/sampler v1.99.99 aurait dû être rétrocompatible avec rsc.io/sampler v1.3.0, mais des bogues ou des hypothèses client incorrectes sur le comportement des modules peuvent se produire.)

La commande go permet à un build d’inclure au maximum une version d’un chemin de module particulier, c’est-à-dire au maximum une version de chaque version majeure : un rsc.io/quote, un rsc.io/quote/v2, un rsc.io/quote/v3, etc. Cela donne aux auteurs de modules une règle claire sur la duplication possible d’un seul chemin de module : il est impossible pour un programme de construire avec rsc.io/quote v1.5.2 et rsc.io/quote v1.6.0. En même temps, le fait d’autoriser différentes versions majeures d’un module (parce qu’elles ont des chemins d’accès différents) permet aux utilisateurs du module de passer progressivement à une nouvelle version majeure. Dans cet exemple, nous voulions utiliser quote.concurrency de rsc/quote/v3 v3.1.0 mais ne sommes pas encore prêts à migrer nos utilisations de rsc.io/quote v1.5.2. La possibilité de migrer progressivement est particulièrement importante dans le cas d’un programme ou d’une base de code de grande envergure.

6. Mettre à niveau une dépendance vers une nouvelle version majeure

Terminons notre conversion de l’utilisation de rsc.io/quote à l’utilisation de rsc.io/quote/v3 seulement. En raison du changement majeur de version, nous devrions nous attendre à ce que certaines API aient été supprimées, renommées ou modifiées d’une manière incompatible. En lisant la documentation, nous pouvons voir que Hello est devenu HelloV3 :

$ go doc rsc.io/quote/v3
package quote // import "rsc.io/quote"

Package quote collects pithy sayings.

func Concurrency() string
func GlassV3() string
func GoV3() string
func HelloV3() string
func OptV3() string
$

(Il y a aussi un bogue connu dans la sortie ; le chemin d’importation affiché a incorrectement laissé tomber le fichier /v3).

Nous pouvons mettre à jour notre utilisation de quote.Hello() dans hello.go pour utiliser quoteV3.HelloV3() :

package hello

import quoteV3 "rsc.io/quote/v3"

func Hello() string {
    return quoteV3.HelloV3()
}

func Proverb() string {
    return quoteV3.Concurrency()
}

Et puis à ce stade, il n’est plus nécessaire de renommer l’importation, donc nous pouvons annuler cela :

package hello

import "rsc.io/quote/v3"

func Hello() string {
    return quote.HelloV3()
}

func Proverb() string {
    return quote.Concurrency()
}

Ré-exécutons les tests pour nous assurer que tout fonctionne :

$ go test
PASS
ok      example.com/hello       0.014s

7. Suppression des dépendances inutilisées

Nous avons supprimé toutes nos utilisations de rsc.io/quote, mais il apparaît toujours dans go list -m all et dans notre fichier go.mod :

$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote v1.5.2
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote v1.5.2
    rsc.io/quote/v3 v3.0.0
    rsc.io/sampler v1.3.1 // indirect
)
$

Pourquoi ? Parce que la compilation d’un seul paquet, comme avec go build ou go test, peut facilement dire quand quelque chose manque et doit être ajouté, mais pas quand quelque chose peut être supprimé en toute sécurité. La suppression d’une dépendance ne peut se faire qu’après avoir vérifié tous les paquets d’un module, et toutes les combinaisons de balises de compilation possibles pour ces paquets. Une commande de compilation ordinaire ne charge pas ces informations, et ne peut donc pas supprimer les dépendances en toute sécurité.

La commande go mod tidy nettoie ces dépendances inutilisées :

$ go mod tidy
$ go list -m all
example.com/hello
golang.org/x/text v0.3.0
rsc.io/quote/v3 v3.1.0
rsc.io/sampler v1.3.1
$ cat go.mod
module example.com/hello

go 1.12

require (
    golang.org/x/text v0.3.0 // indirect
    rsc.io/quote/v3 v3.1.0
    rsc.io/sampler v1.3.1 // indirect
)

$ go test
PASS
ok      example.com/hello    0.020s
$

8. Conclusion

Les modules Go sont l’avenir de la gestion des dépendances dans Go. La fonctionnalité du module est maintenant disponible dans toutes les versions Go supportées (c’est-à-dire dans Go 1.11 et Go 1.12).

Ce post a introduit ces workflows à l’aide des modules Go :

  • go mod init crée un nouveau module, initialisant le fichier go.mod qui le décrit.
  • go build, go test`, et d’autres commandes de construction de paquets ajoutent de nouvelles dépendances à go.mod si nécessaire.
  • go list -m all affiche les dépendances du module courant.
  • go get modifie la version requise d’une dépendance (ou ajoute une nouvelle dépendance).
  • go mod tidy supprime les dépendances inutilisées.

Nous vous encourageons à commencer à utiliser les modules dans votre développement local et à ajouter des fichiers go.mod et go.sum à vos projets. Pour nous faire part de vos commentaires et nous aider à façonner l’avenir de la gestion des dépendances dans Go, veuillez nous envoyer des rapports de bogues ou des rapports d’expérience.

Merci pour tous vos commentaires et votre aide pour l’amélioration des modules.