backend / go
Packages et modules (go.mod)
Explication
Ce que vous allez apprendre
- Comprendre la différence entre un module (
go.mod) et un package (un dossier) - Ajouter et nettoyer des dépendances avec
go getetgo mod tidy - Organiser un projet réel avec
internal/,pkg/etcmd/ - Contrôler la visibilité d'un identifiant grâce à la casse de son nom
- Structurer plusieurs binaires dans un seul projet
Dans quel contexte ?
Une équipe backend fait grandir un projet Go qui tenait au départ dans un seul fichier main.go, jusqu'à devenir un service de plusieurs milliers de lignes. Sans convention d'organisation claire, ce fichier devient vite ingérable, et surtout, du code interne à l'entreprise se retrouve accidentellement importable par n'importe quel autre projet externe — un risque que la convention internal/ élimine structurellement.
D'abord, il faut distinguer deux notions souvent confondues
Un module Go correspond à un seul fichier go.mod, qui déclare son nom (souvent une URL de dépôt Git) et la version minimale de Go requise. Un package, lui, correspond simplement à un dossier : tous les fichiers .go d'un même dossier appartiennent obligatoirement au même package, déclaré en première ligne de chaque fichier.
Un module peut contenir de nombreux packages, mais un projet Go classique n'a qu'un seul go.mod à sa racine.
Une fois cette distinction claire, une convention de dossiers standard émerge
L'écosystème Go a convergé vers une organisation assez uniforme, reconnaissable d'un projet à l'autre.
| Dossier | Rôle | Visibilité |
|---|---|---|
internal/ | Code privé à l'organisation | Bloqué par le compilateur en dehors du module parent |
pkg/ | Code réutilisable, pensé pour être importé ailleurs | Public |
cmd/ | Points d'entrée (un dossier par binaire) | Chacun compile son propre exécutable |
Prérequis
Il faut être à l'aise avec go mod init et la compilation (go build), vues dans la toute première leçon de ce cours.
Il reste une règle de visibilité étonnamment simple à retenir
Go n'a pas de mot-clé public/private : la visibilité d'une fonction, d'un type ou d'une variable dépend uniquement de la casse de la première lettre de son nom. Charger() (majuscule) est exportée, visible depuis n'importe quel autre package qui importe celui-ci. getEnv() (minuscule) reste strictement privée à son propre package.
Piège fréquent
Renommer accidentellement une fonction publique en minuscule (ou l'inverse) casse silencieusement la compilation de tous les packages qui l'importaient, avec un message d'erreur qui peut sembler déroutant au premier abord ("undefined: config.charger"). Vérifie toujours la casse en priorité face à une erreur d'import inattendue.
Ensuite, le dossier internal/ bénéficie d'une protection unique dans Go
Contrairement à pkg/ qui n'est qu'une convention de nommage sans effet technique, internal/ est reconnu spécifiquement par le compilateur Go : aucun package en dehors du module qui le contient (ou d'un sous-arbre précis) ne peut l'importer, même s'il le voulait. C'est une garantie d'encapsulation au niveau du langage, pas seulement une bonne pratique.
Bonne pratique
Exécute go mod tidy régulièrement, surtout après avoir ajouté ou retiré un import : cette commande synchronise go.sum avec les dépendances réellement utilisées dans le code, en retirant celles devenues inutiles et en complétant celles qui manquent.
Maintenant que ton projet est bien structuré, il devient indispensable de vérifier automatiquement que chaque package fonctionne comme prévu : place aux tests, avec le pattern le plus idiomatique de Go pour ça, les table-driven tests.
Commandes & code
Packages et modules (go.mod)
Organiser un projet Go réel : modules, packages internes, dépendances versionnées.
# go.mod : declare le module racine et ses dependances
go mod init github.com/monorg/monprojet
# Ajouter une dependance externe
go get github.com/gin-gonic/gin@v1.9.1
# Nettoyer les dependances inutilisees et completer le go.sum
go mod tidy
# Voir l'arbre des dependances
go mod graphmonprojet/
go.mod
go.sum
main.go
internal/ <- inaccessible depuis l'exterieur du module (convention Go)
config/
config.go
service/
user_service.go
pkg/ <- code reutilisable, expose publiquement
validator/
validator.go
cmd/ <- points d'entree (plusieurs binaires possibles)
api/
main.go
worker/
main.go// fichier: internal/config/config.go
package config // le nom du package = nom du dossier, par convention
import (
"os"
)
type Config struct {
Port string
DatabaseURL string
}
// Charger : exportee (Majuscule) = visible hors du package
func Charger() Config {
return Config{
Port: getEnv("PORT", "8080"),
DatabaseURL: getEnv("DATABASE_URL", "postgres://localhost/app"),
}
}
// getEnv : non exportee (minuscule) = privee au package
func getEnv(cle, defaut string) string {
if v := os.Getenv(cle); v != "" {
return v
}
return defaut
}// fichier: main.go
package main
import (
"fmt"
"github.com/monorg/monprojet/internal/config"
)
func main() {
cfg := config.Charger()
fmt.Println("demarrage sur le port", cfg.Port)
}Résumé
- Un module = un
go.mod; un package = un dossier, tous les fichiers y déclarentpackage xxx. - La visibilité se fait par la casse :
Exporte(public) vsnonExporte(privé au package). internal/est réservé par le compilateur : inaccessible depuis l'extérieur du module.go mod tidymaintientgo.sumcohérent avec les imports réellement utilisés.
Exercices pratiques
Mission : un partenaire externe importe du code censé rester interne
Objectif : Restructurer un projet Go monolithique pour protéger son code interne, corriger un import cassé par la casse, et distinguer module et package.
Contexte
L'équipe plateforme découvre qu'une startup partenaire importe directement github.com/monorg/monprojet/service dans son propre projet Go, un package qui contient la logique métier de facturation censée rester strictement interne à l'entreprise. Rien dans le code actuel ne l'empêche techniquement.