Go modules : gestion des dépendances et versioning

1. Aperçu

Depuis Go 1.11, Go modules est le mécanisme officiel de gestion des dépendances. Depuis Go 1.16, c’est le mode par défaut, remplaçant complètement l’ancienne approche basée sur GOPATH. Cet article parcourt tout le cycle de vie d’un module : initialisation, ajout de dépendances, sélection et mise à niveau de version, gestion des versions majeures v2+, et travail avec du code local ou des dépôts privés. Après lecture, vous comprendrez ce que contiennent go.mod et go.sum. Vous saurez aussi pourquoi Go choisit une version spécifique et comment corriger les erreurs de dépendances courantes.

2. Initialisation du module

Un module est un répertoire contenant un fichier go.mod à la racine. Créez un nouveau module avec go mod init :

mkdir order-service && cd order-service
go mod init github.com/nonla/order-service

Le fichier go.mod est créé :

module github.com/nonla/order-service

go 1.24

Signification de chaque ligne :

  • module est le chemin du module. C’est le préfixe d’import pour tous les packages à l’intérieur du module. Si le module doit être go get par d’autres, ce chemin doit correspondre à l’URL du dépôt.
  • go 1.24 est la version du langage minimale que le module requiert. Depuis Go 1.21, cette ligne est obligatoire. Si votre toolchain est plus ancien, Go téléchargera automatiquement le toolchain approprié (si GOTOOLCHAIN=auto) ou signalera une erreur.

Remarque : Pour les applications internes qui ne seront jamais importées de l’extérieur, vous pouvez utiliser un chemin court comme order-service. Toutefois, utiliser un chemin de type URL dès le départ évite de devoir modifier de nombreux chemins d’import par la suite.

3. Ajouter des dépendances

3.1. Utiliser go get

Supposons que vous ayez besoin de générer un ID pour les commandes avec UUID. Ajoutez la bibliothèque github.com/google/uuid :

go get github.com/google/uuid@v1.6.0

Écrivez ensuite le code utilisant cette bibliothèque :

// order/order.go
package order

import "github.com/google/uuid"

type Order struct {
	ID       string
	Customer string
	Total    int64 // unité : VND
}

func NewOrder(customer string, total int64) Order {
	return Order{
		ID:       uuid.NewString(),
		Customer: customer,
		Total:    total,
	}
}

go.mod contient maintenant un bloc require :

module github.com/nonla/order-service

go 1.24

require github.com/google/uuid v1.6.0

Vous pouvez aussi sauter l’étape go get : écrivez l’import en premier, puis exécutez go mod tidy. Go trouvera automatiquement la version la plus récente et l’ajoutera à go.mod.

3.2. Fichier go.sum

Au même moment, Go crée le fichier go.sum :

github.com/google/uuid v1.6.0 h1:<hash du contenu du module>
github.com/google/uuid v1.6.0/go.mod h1:<hash du fichier go.mod>

Chaque dépendance a deux lignes. La première est le hash du contenu complet du module, la seconde est le hash du fichier go.mod. Lors du téléchargement du module, Go compare le hash réel avec celui de go.sum. Il effectue aussi une vérification avec la base de données de checksum publique (sum.golang.org). Si quelqu’un modifie le contenu d’une balise déjà publiée, le build échouera plutôt que d’utiliser silencieusement du code étrange.

Remarque : Commitez toujours go.mod et go.sum. N’ajoutez pas go.sum à .gitignore. Sans ce fichier, votre CI ne pourra pas vérifier les dépendances.

3.3. Dépendances directes et indirectes

Ajoutez testify pour écrire des tests :

go get github.com/stretchr/testify@v1.10.0

go.mod contiendra maintenant deux blocs require :

require (
	github.com/google/uuid v1.6.0
	github.com/stretchr/testify v1.10.0
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

Le commentaire // indirect marque les modules que votre code n’importe pas directement mais qui sont nécessaires pour d’autres dépendances. Go les ajoute à go.mod pour que le build soit entièrement reproductible. De cette façon, Go n’a pas besoin de relire go.mod de chaque dépendance pour construire le graphe.

4. Versioning et sélection de version par Go

4.1. Semantic versioning

Go modules utilise le semantic versioning au format vMAJOR.MINOR.PATCH :

  • PATCH (v1.6.0 → v1.6.1) : correction de bug, pas de changement d’API.
  • MINOR (v1.6.0 → v1.7.0) : ajout de fonctionnalité, rétro-compatibilité conservée.
  • MAJOR (v1 → v2) : changements rompant la compatibilité.

Les versions doivent être des balises git avec le préfixe v. Si vous pointez vers un commit sans balise, Go crée une pseudo-version :

go get github.com/google/uuid@0f3b7a2
github.com/google/uuid v1.6.1-0.20240815101530-0f3b7a2c1d4e

La pseudo-version se compose de trois parties : version de base, timestamp UTC du commit et 12 premiers caractères du hash. Ainsi, la pseudo-version reste correctement ordonnée par le temps.

4.2. Minimal Version Selection

C’est la principale différence par rapport à npm ou Maven. Go utilise l’algorithme Minimal Version Selection (MVS). Pour chaque module, Go choisit la version la plus élevée parmi les versions requises, pas la plus récente disponible.

Par exemple, votre projet a deux dépendances :

order-service
├── payment-sdk  → require github.com/google/uuid v1.3.0
└── shipping-sdk → require github.com/google/uuid v1.5.0

Même si uuid v1.6.0 est déjà publié, Go choisira v1.5.0. Raison : c’est la version la plus basse qui satisfait tous les requirements. Conséquences :

  • Le build est reproductible. Une nouvelle version d’une bibliothèque ne changera pas automatiquement votre build.
  • Pas besoin de fichier lock séparé. go.mod suffit pour déterminer les versions.
  • Pour utiliser une version plus récente, vous devez le faire délibérément.

Consultez les versions réellement sélectionnées avec :

go list -m all
github.com/nonla/order-service
github.com/davecgh/go-spew v1.1.1
github.com/google/uuid v1.5.0
...

4.3. Mise à niveau et rétrogradation

Vérifiez quelle dépendance a une nouvelle version :

go list -m -u all
github.com/google/uuid v1.5.0 [v1.6.0]
github.com/stretchr/testify v1.10.0

La version entre crochets est la plus récente vers laquelle vous pouvez mettre à niveau. Les commandes de mise à niveau courantes :

go get github.com/google/uuid@latest   # version la plus récente
go get github.com/google/uuid@v1.5.0   # vers une version exacte (peut être une rétrogradation)
go get -u=patch ./...                  # mettre à niveau uniquement PATCH pour toutes les dépendances
go get -u ./...                        # mettre à niveau MINOR + PATCH pour toutes les dépendances
go get github.com/google/uuid@none     # supprimer complètement la dépendance

Remarque : go get -u ./... met à niveau aussi les dépendances indirectes. Le diff peut être énorme. Sur un projet production, vous devriez mettre à niveau module par module, exécuter les tests, puis commiter.

5. Major version v2 et supérieur

Go applique la règle d’import compatibility : deux versions avec le même import path doivent être rétro-compatibles. Par conséquent, à partir de v2, la version majeure fait partie du chemin du module.

Exemple avec le routeur chi :

go get github.com/go-chi/chi/v5@v5.1.0
package main

import (
	"net/http"

	"github.com/go-chi/chi/v5"
)

func main() {
	r := chi.NewRouter()
	r.Get("/orders", func(w http.ResponseWriter, _ *http.Request) {
		w.Write([]byte("liste des commandes"))
	})
	http.ListenAndServe(":8080", r)
}

Le suffixe /v5 est dans le chemin d’import. Le nom du package reste chi, donc vous appelez chi.NewRouter() normalement. Comme chi/v4 et chi/v5 ont des chemins différents, les deux versions peuvent coexister dans un même binaire. Cela vous aide à migrer progressivement.

Si vous êtes auteur de bibliothèque et voulez publier v2 :

module github.com/nonla/pricing/v2

go 1.24

Vous changez la ligne module en chemin avec suffixe /v2, mettez à jour les imports internes, puis taguez v2.0.0.

Remarque : Une erreur courante est de tagger v2.0.0 en oubliant de changer le chemin du module. Alors go get github.com/nonla/pricing@v2.0.0 échouera avec : invalid version: module contains a go.mod file, so module path must match major version.

6. Commandes go mod utiles

go mod tidy      # ajouter les modules manquants, supprimer les modules inutilisés
go mod why -m gopkg.in/yaml.v3   # pourquoi ce module est-il présent
go mod graph     # afficher le graphe complet des dépendances
go mod verify    # vérifier que le cache local correspond à go.sum
go mod download  # télécharger par avance les dépendances (utile pour Docker)

Exemple de sortie de go mod why :

# gopkg.in/yaml.v3
github.com/nonla/order-service/order
github.com/stretchr/testify/assert
gopkg.in/yaml.v3

À lire de haut en bas : le package order (dans les tests) importe testify/assert, qui lui-même importe yaml.v3.

Exécutez go mod tidy avant chaque commit. En CI, vous pouvez vérifier que go.mod est bien rangé :

go mod tidy -diff

Depuis Go 1.23, le flag -diff affiche les différences et retourne un code de sortie non-zéro si le fichier n’est pas tidy. Cette commande ne modifie pas le fichier.

7. Travailler avec du code local : replace et go.work

7.1. Directive replace

Supposons que vous corrigiez la bibliothèque pricing dans un répertoire à côté et que vous vouliez que order-service utilise immédiatement votre version en cours de correction :

require github.com/nonla/pricing v1.2.0

replace github.com/nonla/pricing => ../pricing

Go lira le code depuis ../pricing au lieu de télécharger depuis le réseau. replace peut aussi pointer vers un fork :

replace github.com/nonla/pricing => github.com/minh/pricing v1.2.1-fix

Remarque : replace n’a d’effet que dans le module principal. Si votre module est une bibliothèque, ses utilisateurs ne verront pas votre directive replace. De plus, ne committez pas de replace pointant vers un chemin local. Le build en CI cassera car le répertoire ../pricing n’existera pas là.

7.2. Workspace avec go.work

Depuis Go 1.18, workspace est une façon plus propre de développer plusieurs modules simultanément :

go work init ./order-service ./pricing

Le fichier go.work est créé :

go 1.24

use (
	./order-service
	./pricing
)

Quand vous exécutez go build ou go test dans le workspace, tout import vers github.com/nonla/pricing utilisera le code local. Vous n’avez pas besoin de modifier go.mod. Généralement, go.work n’est pas commité, chaque développeur le crée selon l’organisation de ses répertoires.

7.3. exclude et retract

exclude s’utilise dans le module principal pour exclure une version spécifique :

exclude github.com/google/uuid v1.4.0

retract est déclaré par l’auteur de la bibliothèque pour indiquer qu’une version déjà publiée ne devrait pas être utilisée :

retract v1.2.0 // calcul incorrect de la TVA pour les commandes internationales

Une fois que vous publiez une nouvelle version contenant cette ligne, go get @latest ignorera v1.2.0. La commande go list -m -u avertira aussi ceux qui utilisent cette version.

8. Modules privés et proxy

Par défaut, Go télécharge les modules via GOPROXY=https://proxy.golang.org,direct et vérifie les checksums via sum.golang.org. Avec des dépôts internes, le proxy public n’y a pas accès, et vous ne voulez pas non plus révéler le nom du module. Déclarez :

go env -w GOPRIVATE=github.com/nonla/*,gitlab.nonla.internal/*

Les modules correspondant aux patterns dans GOPRIVATE seront téléchargés directement via git et contourneront la base de données de checksums. Vous devez toujours configurer l’authentification git, par exemple avec ~/.netrc ou SSH :

git config --global url."git@github.com:".insteadOf "https://github.com/"

Si votre entreprise dispose d’un proxy personnel (Athens, Artifactory…), mettez ce proxy au début de GOPROXY :

go env -w GOPROXY=https://goproxy.nonla.internal,https://proxy.golang.org,direct

9. Tester le comportement avec les dépendances

Écrivez un test pour NewOrder, en utilisant testify ajouté à la section 3.3 :

// order/order_test.go
package order

import (
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestNewOrder_GeneratesValidUniqueID(t *testing.T) {
	first := NewOrder("Nguyễn Văn An", 250_000)
	second := NewOrder("Trần Thị Bình", 120_000)

	_, err := uuid.Parse(first.ID)
	require.NoError(t, err)

	assert.NotEqual(t, first.ID, second.ID)
	assert.Equal(t, "Nguyễn Văn An", first.Customer)
	assert.EqualValues(t, 250_000, first.Total)
}

Exécutez le test :

go test ./...
ok  	github.com/nonla/order-service/order	0.004s

Le test vérifie trois choses : l’ID est un UUID valide, deux commandes ont des ID différents, et les autres champs sont assignés correctement. require.NoError arrête le test immédiatement si l’ID ne peut pas être parsé. assert enregistre l’erreur et continue l’exécution. Lors de la première exécution sur une nouvelle machine, Go téléchargera les dépendances selon les versions exactes de go.mod et vérifiera les hashs avec go.sum avant la compilation.

10. Erreurs courantes

  • missing go.sum entry for module providing package : il manque une ligne dans go.sum, généralement parce que quelqu’un a modifié go.mod à la main. Solution : exécutez go mod tidy.
  • checksum mismatch : le contenu du module ne correspond pas au hash. Essayez go clean -modcache. Si l’erreur persiste avec un module public, c’est un signal d’alerte grave et ne supprimez pas la ligne dans go.sum pour contourner le problème.
  • go: updates to go.mod needed : la CI exécute souvent avec -mod=readonly, tandis que go.mod n’est pas tidy. Vous exécutez go mod tidy en local puis committez à nouveau.
  • Import d’une mauvaise version majeure : utiliser github.com/go-chi/chi au lieu de .../chi/v5. Vous recevrez une vieille version v1 ou une pseudo-version étrange. Vérifiez d’abord le chemin d’import.
  • Abus de vendor/ : go mod vendor est toujours utile pour un build entièrement hors ligne. Sinon, un proxy combiné à go.sum assure déjà la reproductibilité, donc vous n’avez pas besoin de commiter des milliers de fichiers vendor.

11. Conclusion

Go modules résout le problème des dépendances avec quelques idées simples. go.mod déclare les requirements, go.sum assure l’intégrité, et Minimal Version Selection garantit la reproductibilité sans avoir besoin d’un fichier lock. La version majeure est incluse dans le chemin d’import, donc les changements rompant la compatibilité sont toujours visibles dans le code.

Pour le travail quotidien, vous ne devez retenir que quatre habitudes :

  • Utilisez go get module@version pour ajouter ou mettre à niveau intentionnellement une dépendance.
  • Exécutez go mod tidy avant de commiter.
  • Utilisez go.work à la place de replace lors du développement de plusieurs modules simultanément.
  • Configurez GOPRIVATE pour vos dépôts internes.
golanggo modulesdependencyversioning