Featured image of post Structure de projet Go : ce qui fonctionne vraiment

Structure de projet Go : ce qui fonctionne vraiment

Patterns pratiques pour structurer vos projets Go, basés sur Helm, Hugo et Prometheus, pas sur le 'standard' non officiel.

La communauté Go n’a pas de structure de projet officielle. C’est intentionnel. Pourtant, les développeurs passent des heures à débattre de pkg/ vs packages à la racine, alors que la réponse dépend de ce que vous construisez.

Voici une approche pratique pour structurer vos projets Go, basée sur les patterns de Helm, Hugo, Prometheus et de projets plus modestes.

La seule règle qui compte

Go impose exactement une règle structurelle : les packages dans internal/ ne peuvent pas être importés depuis l’extérieur du module. Tout le reste est convention.

La documentation officielle sur go.dev/doc/modules/layout dit : commencez simple. Un main.go et un go.mod suffisent pour les petits projets. Ajoutez de la structure quand vous en avez besoin, pas avant.

Trois patterns qui fonctionnent

Bibliothèque à la racine

Pour les projets destinés à être importés par d’autres.

1
2
3
4
5
6
7
mylib/
├── go.mod
├── mylib.go            # API principale
├── option.go           # Options fonctionnelles
├── types.go            # Types publics
└── internal/           # Helpers privés
    └── util/

Les utilisateurs importent directement :

1
2
3
import "github.com/user/mylib"

client := mylib.New(mylib.WithTimeout(5 * time.Second))

Exemples : Cobra, Viper, goldmark.

Bibliothèque + CLI

Pour les projets offrant à la fois une bibliothèque et un outil en ligne de commande.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
myproject/
├── go.mod
├── service.go          # API publique
├── types.go            # Types publics
├── internal/
   ├── config/         # Parsing de config
   └── util/           # Helpers privés
└── cmd/
    └── mytool/         # Binaire CLI
        └── main.go

Le CLI est une fine couche autour de la bibliothèque. Les utilisateurs peuvent :

1
2
3
4
5
# Importer la bibliothèque
go get github.com/user/myproject

# Installer le CLI
go install github.com/user/myproject/cmd/mytool@latest

Exemples : Helm, Hugo, Prometheus.

CLI uniquement

Pour les outils qui ne sont pas destinés à être importés comme bibliothèques.

1
2
3
4
5
6
7
8
mytool/
├── go.mod
├── main.go
├── internal/
   ├── cmd/            # Implémentations des commandes
   ├── config/         # Configuration
   └── core/           # Logique métier
└── testdata/

Tout dans internal/ signale : ceci est une application, pas une bibliothèque. Terraform utilise cette approche.

Pourquoi pas pkg/ ?

Le répertoire pkg/ ajoute un segment de chemin sans aucun bénéfice :

1
2
3
4
5
// Avec pkg/
import "github.com/user/project/pkg/thing"

// Sans pkg/
import "github.com/user/project/thing"

Le second est plus court et plus clair. L’équipe Go ne recommande explicitement pas pkg/. Russ Cox, le responsable technique de Go, a critiqué le repo golang-standards/project-layout (54k+ stars) pour promouvoir des patterns que l’équipe Go n’a jamais approuvés.

Les projets modernes (Hugo, Prometheus, Cobra) n’utilisent pas pkg/.

Quand utiliser internal/

Utilisez internal/ pour du code qui :

  • Supporte votre API publique mais ne devrait pas être exposé
  • Peut changer sans préavis
  • N’a pas de garanties de stabilité

Candidats courants :

  • Parsing de configuration
  • Fonctions utilitaires (manipulation de fichiers, chaînes)
  • Implémentations de protocoles
  • Helpers de test

N’utilisez pas internal/ pour :

  • Les petits projets où les fonctions non exportées suffisent
  • Du code que vous pourriez vouloir exposer plus tard (sortir de internal/ est un breaking change pour vos imports)

Laurent Demailly va plus loin : n’utilisez pas internal/ sauf si vous livrez à de nombreux utilisateurs tiers avec beaucoup de code partagé. Son argument : la plupart du code n’a pas besoin d’être caché, et les fonctions non exportées dans un package suffisent généralement. C’est une position valide pour les applications et les petites bibliothèques.

Quand créer un nouveau package

Créez un package quand :

  • Plusieurs fichiers partagent une responsabilité distincte
  • Le code a ses propres types et fonctions formant une unité cohésive
  • Vous voulez contrôler la visibilité aux frontières du package

Ne créez pas de package pour :

  • Un seul fichier avec quelques fonctions helper
  • De l’organisation sans bénéfice fonctionnel
  • Copier la structure d’un projet externe

Un fichier de 200 lignes à la racine, c’est bien. Un package utils/ avec trois fonctions, c’est de la sur-ingénierie.

Le processus de décision

Quand vous ajoutez du code :

QuestionOuiNon
Les utilisateurs externes doivent importer ?Package racineinternal/
C’est spécifique au CLI ?cmd/appname/Code bibliothèque
Cela nécessite son propre package ?Seulement si plusieurs fichiers avec responsabilité distincteGarder dans le package existant

Organisation des fichiers à la racine

Pour les projets bibliothèque, découpez par responsabilité :

1
2
3
4
5
6
7
8
mylib/
├── client.go           # Type Client et méthodes
├── option.go           # Options fonctionnelles
├── types.go            # Types publics
├── errors.go           # Erreurs sentinelles
├── request.go          # Construction de requêtes
├── response.go         # Parsing de réponses
└── mylib_test.go       # Tests

Chaque fichier a une seule responsabilité. Vous cherchez les définitions d’erreurs ? Regardez errors.go. La configuration ? Regardez option.go.

Évitez :

  • util.go (nommez-le selon ce qu’il fait)
  • helpers.go (même problème)
  • misc.go (là où le code va mourir)

Considérations mono-repo

Pour les projets avec plusieurs binaires :

1
2
3
4
5
6
7
8
9
myproject/
├── go.mod              # Module unique
├── lib/                # Code bibliothèque partagé
├── cmd/
│   ├── server/
│   ├── worker/
│   └── cli/
└── internal/
    └── shared/         # Code interne partagé

Évitez les repos multi-modules (plusieurs fichiers go.mod) sauf raison impérieuse. Ils compliquent :

  • Le développement local (besoin de directives replace)
  • Le versioning (les tags doivent inclure le chemin du module)
  • Les tests (go test ./... ne fonctionne pas entre modules)

L’équipe Go recommande les repos single-module pour la plupart des projets.

Exemples réels

ProjetStructureBibliothèque importable ?
CobraPackage racineOui, github.com/spf13/cobra
Helmpkg/ + cmd/Oui, helm.sh/helm/v3/pkg/action
HugoRacine + hugolib/Oui, github.com/gohugoio/hugo/hugolib
PrometheusPackages racineOui, github.com/prometheus/prometheus/promql
TerraformTout dans internal/Non, CLI uniquement

Erreurs courantes

  • Sur-structurer trop tôt : commencer avec pkg/, internal/, cmd/, api/, web/ pour un projet de 500 lignes. Commencez à plat, ajoutez de la structure quand la douleur apparaît.
  • Copier golang-standards/project-layout : ce repo n’est pas approuvé par l’équipe Go. Il promeut des patterns que la plupart des projets Go n’utilisent pas.
  • Packages vides pour usage futur : si un package a un fichier avec deux fonctions, ce n’est pas un package. C’est un fichier.
  • internal/ pour tout : si rien n’est importable, votre projet est une application, pas une bibliothèque. C’est bien, mais soyez intentionnel.

Résumé

  • Commencez simple : main.go + go.mod est valide
  • Code bibliothèque à la racine : chemins d’import propres
  • CLI dans cmd/ : fine couche autour de la bibliothèque
  • Code privé dans internal/ : détails d’implémentation
  • Évitez pkg/ : ça n’apporte rien
  • Un seul module : évitez la complexité multi-module

La meilleure structure est celle à laquelle vous ne pensez pas. Si vous passez du temps sur la structure plutôt que sur le code, vous sur-ingéniez.