Cherchez « structure projet Go » et, en deux clics, vous tombez sur golang-standards/project-layout, un dépôt GitHub qui compte plus de 50 000 étoiles et une arborescence remplie de cmd/, pkg/, internal/, api/ et build/. Il a l'air de faire autorité, et la plupart des débutants le recopient tel quel pour un programme de cinq fichiers.
Ce n'est pourtant pas un standard officiel, et l'équipe Go l'a dit publiquement. Les recommandations officielles sont plus simples : commencez à plat, et n'ajoutez des dossiers que lorsque le code en a besoin. Les sections suivantes suivent ces recommandations un dossier à la fois, avec une CLI météo et une API de librairie comme exemples fil rouge.
Quelle est la structure standard d'un projet Go ?
Il n'existe pas d'arborescence unique imposée. Go n'exige ni src/ ni cmd/, ni aucun dossier en dehors d'un fichier go.mod à la racine du module. Le guide officiel, « Organizing a Go module », lie la structure à la taille du projet. Il présente quatre structures, chacune un cran au-dessus de la précédente : un package simple, une commande simple, un package qui se dote de packages annexes, et un projet « serveur » à plusieurs binaires. (source)
La vraie question est donc de savoir de combien de structure votre projet a besoin maintenant, et pour la plupart des programmes, la réponse est : très peu. La suite de cet article passe en revue les quatre structures, de la plus simple à la plus élaborée. Trouvez celle qui correspond à votre projet et arrêtez-vous là.
Commencer à plat : un seul package, c'est idiomatique
Les deux premières structures de la documentation officielle sont un package unique et une commande unique, placés directement à la racine du module, sans sous-répertoire. Pour un petit outil en ligne de commande qui lit le nom d'une ville et affiche la météo, voici le projet complet :
example.texttextweather/ ├── go.mod ├── main.go ├── weather.go └── weather_test.go
go.mod déclare le chemin du module (par exemple module github.com/you/weather). main.go contient package main et le point d'entrée. weather.go contient la logique, dans le même package. weather_test.go contient les tests, juste à côté du code qu'ils couvrent, là où Go s'attend à les trouver. Vous pouvez livrer le projet en l'état. La documentation officielle appelle cela une « basic command », et c'est la bonne forme pour la plupart des outils que vous écrirez pendant votre première année.
Une règle explique la plupart des choix de structure qui viennent ensuite : un répertoire est un package. Chaque fichier d'un répertoire déclare le même package, et le nom du package est le dernier élément du chemin d'import. Ajoutez un répertoire weather/store/, et chaque fichier qu'il contient indique package store. Vous l'importez sous la forme github.com/you/weather/store et vous l'appelez avec store.Get.
N'ajoutez pas encore de dossiers. Un répertoire utils/ qui contient une seule fonction, ou un répertoire models/ qui contient un seul struct, ajoute un chemin d'import et une frontière de package sans rien vous apporter en retour. Gardez le projet à plat jusqu'à ce que cette organisation pose de vrais problèmes.
Quand ajouter un répertoire internal/ ?
La première bonne raison d'ajouter de la structure, c'est la confidentialité. Go a deux niveaux de visibilité pour les identifiants : les noms qui commencent par une majuscule sont exportés, et ceux en minuscule sont privés au package. Cela fonctionne au sein d'un package. Mais dès que votre module compte plusieurs packages, vous voulez souvent du code que tous les packages à l'intérieur de votre module peuvent utiliser et que rien à l'extérieur ne peut importer. C'est exactement ce que vous apporte internal/, et le compilateur le fait respecter.
Cela fait partie du langage, ce n'est pas une convention de nommage. Un package situé sous un répertoire nommé internal/ ne peut être importé que par du code enraciné dans le répertoire parent de ce internal/, et un import depuis un autre module fait échouer le build. La documentation officielle recommande de garder les packages dans internal « autant que possible ». Le code extérieur ne peut pas les importer, il ne peut donc pas en dépendre, et vous ne vous retrouvez jamais à maintenir une API que vous n'aviez pas l'intention de publier. (source)
Imaginons que la CLI météo devienne une petite API HTTP pour une librairie. Elle comporte désormais une authentification des requêtes et une couche d'accès aux données, et ni l'une ni l'autre n'a sa place dans une API publique :
example.texttextbookstore/ ├── go.mod ├── main.go └── internal/ ├── auth/ │ ├── auth.go │ └── auth_test.go └── store/ ├── store.go └── store_test.go
main.go se trouve dans le module bookstore, il peut donc importer librement github.com/you/bookstore/internal/auth et github.com/you/bookstore/internal/store. Le code de tout autre module ne le peut pas. Si quelqu'un lance go get sur votre module et tente d'importer ces packages, le compilateur rejette l'import. Comme personne en dehors de votre module ne peut dépendre de auth ou de store, vous pouvez renommer leurs fonctions, modifier leurs signatures ou les réorganiser quand vous le souhaitez, sans casser le code de qui que ce soit.
Quand ajouter un répertoire cmd/ ?
cmd/ sert aux dépôts qui produisent plus d'un binaire, ou qui livrent à la fois un binaire et une bibliothèque importable. Si vous n'avez qu'un seul binaire, vous n'en avez pas besoin, et un main.go à la racine est plus propre.
Sous cmd/, chaque binaire a son propre sous-répertoire, et le nom du répertoire devient le nom du programme. C'est ce que fait la structure officielle « server project » : chaque commande se trouve dans cmd/<name>/ avec un petit main.go qui assemble des packages de internal/. (source) Supposons que la librairie livre maintenant un serveur d'API et un outil de migration de base de données :
example.texttextbookstore/ ├── go.mod ├── cmd/ │ ├── api/ │ │ └── main.go │ └── migrate/ │ └── main.go └── internal/ ├── auth/ └── store/
go build ./cmd/api produit un binaire nommé api, et go build ./cmd/migrate produit migrate. Les deux fichiers main.go restent légers et importent le vrai travail depuis internal/.
Voici toute la progression dans un seul tableau. Choisissez la ligne qui correspond à votre projet tel qu'il est aujourd'hui, pas au projet que vous imaginez qu'il deviendra.
| Forme du projet | Structure recommandée | Pourquoi |
|---|---|---|
| Petit outil ou script | À plat : main.go + fichiers de logique à la racine | Un seul package est idiomatique et complet |
| Un binaire avec du code interne réutilisable | Ajouter internal/ pour les packages privés | Confidentialité garantie par le compilateur, pas d'API prématurée |
| Deux binaires ou plus | Ajouter cmd/<name>/ par binaire | Chaque main a son propre répertoire |
| Dépôt multiservice (binaires + code partagé) | cmd/ + internal/, découpés par domaine | La structure officielle « server project » |
Le mythe de golang-standards/project-layout
Le dépôt golang-standards/project-layout porte le mot « standards » dans le nom de son organisation GitHub, compte plus de 50 000 étoiles et propose une grande arborescence avec api/, build/, configs/, deployments/, pkg/ et test/. On suppose facilement que l'équipe Go l'a approuvé, mais ce n'est pas le cas. Le README du dépôt affiche désormais un avertissement précisant qu'il ne s'agit pas d'un standard officiel, et l'objection est venue de la direction même de Go.
Dans l'issue #117, Russ Cox, tech lead de Go, a écrit qu'il ne s'agit « pas d'une structure standard de projet Go », et que la plupart des dépôts Go sont « beaucoup plus simples » et n'utilisent pas du tout de répertoire pkg/. Le fil a fait la une de Hacker News, où les développeurs Go expérimentés lui ont majoritairement donné raison. Cette structure peut avoir du sens pour un gros monorepo partagé par plusieurs équipes, mais comme modèle de départ pour un projet ordinaire, elle fait du tort. Recopiez-la dans un petit programme, et vos cinq fichiers se retrouvent répartis sous huit dossiers presque vides.
Quelques règles en découlent, et elles rejoignent ce que conseillent couramment des praticiens de Go comme Eli Bendersky et Alex Edwards :
- N'utilisez pas de répertoire
src/. Cette habitude vient de Java et d'anciens projets Node. Le code Go se trouve à la racine du module, et aucune structure officielle ne comporte desrc/. - N'imbriquez pas de dossiers avant d'avoir du code à y mettre. Des dossiers
api/,build/etdeployments/vides ne servent à rien. Créez un dossier le jour où vous avez quelque chose à y placer. - Nommez les packages d'après ce qu'ils fournissent, pas d'après leur rôle. Un package appelé
utils,commonouhelpersn'apprend rien au lecteur et devient un fourre-tout. Appelez-lestore,auth,weather,retryouslugify. Les recommandations officielles de Go sur les noms de packages sont de les garder courts et clairs, et de faire en sorte qu'ils se lisent bien au point d'appel :store.Getvaut mieux queutils.GetFromStore. (source)
Pour les erreurs qui apparaissent dans le code plutôt que dans les dossiers, consultez notre guide des erreurs Go courantes à éviter.
Organiser un projet Go : la règle simple pour débuter
La progression se résume à quelques questions sur votre projet. Parcourez-les dans l'ordre, et n'ajoutez un répertoire que lorsqu'une réponse l'exige.
flowchart TD A["New Go project"] --> B["Is it one package<br/>of related code?"] B -->|Yes| C["Keep it flat:<br/>main.go + logic in the root"] B -->|"Grew past that"| D["Need to hide code<br/>from outside importers?"] D -->|Yes| E["Add internal/<br/>put private packages there"] E --> F["More than one<br/>binary to build?"] F -->|No| G["Done. main.go in root,<br/>logic in internal/"] F -->|Yes| H["Add cmd/name/<br/>one dir per binary"] H --> I["Many services sharing code?"] I -->|Yes| J["Server layout:<br/>cmd/ + internal/ by domain"]
Chaque étape répond à un besoin précis : une frontière de package, un deuxième binaire ou du code que vous voulez garder privé. Si vous ne pouvez désigner aucun de ces besoins, vous n'avez probablement pas besoin du dossier. Commencez à plat, ajoutez internal/ quand vous voulez que le compilateur garantisse la confidentialité, ajoutez cmd/ quand vous avez un deuxième binaire, et ne passez à la structure serveur complète que lorsque le dépôt contient réellement plusieurs services.
Questions fréquentes
Existe-t-il une structure officielle pour un projet Go ?
Non, il n'existe pas d'arborescence unique obligatoire. Le document officiel, « Organizing a Go module », décrit une structure qui grandit avec la taille du projet plutôt qu'une structure figée. Elle part d'un package à plat, puis passe à internal/, ensuite à cmd/, et enfin à une structure serveur complète. Utilisez la plus petite structure qui convient à votre projet. (source)
Faut-il utiliser le répertoire pkg/ ?
En général, non. Russ Cox, tech lead de Go, a fait remarquer que la plupart des dépôts Go n'utilisent pas de répertoire pkg/ et « ont tendance à être beaucoup plus simples ». Placez le code privé dans internal/, où le compilateur garantit la confidentialité, et n'exposez des packages à la racine du module que si vous comptez les maintenir comme API publique. (source)
Que mettre dans internal/ et dans cmd/ ?
internal/ contient du code de bibliothèque importable que seul votre module peut utiliser, et le compilateur le fait respecter. cmd/<name>/ contient le package main, léger, de chaque binaire que vous produisez, qui assemble le code de internal/. La logique va dans internal/, et les points d'entrée dans cmd/. (source)
Un vrai projet Go peut-il tenir en un seul fichier ou un seul package ?
Oui. Le premier exemple du guide officiel sur la structure est un package unique à la racine du module, et de nombreux outils en production sont livrés ainsi. Un seul package avec un go.mod, un main.go et ses tests est idiomatique et complet. Ajoutez de la structure quand le code devient trop grand pour elle. (source)
En quoi un projet Go diffère-t-il d'un projet Java ou Node ?
Go n'a pas de répertoire src/. Le code se trouve à la racine du module, chaque répertoire est un package, et le nom du package est le dernier élément du chemin. Aucun outil de build n'impose d'arborescence comme le font Maven ou un bundler, si bien que les structures Go restent plus plates et ne grandissent qu'en cas de besoin. (source)
Pour aller plus loin
La structure est l'un des aspects les plus simples de Go. Vous livrez un programme à plat, puis vous ajoutez internal/ et cmd/ quand le code en a besoin. Si vous ne savez pas dire pourquoi un dossier existe, vous pouvez probablement le supprimer.
Le meilleur moyen de vous faire la main est de construire deux ou trois vrais programmes, par exemple une CLI et un service HTTP, et de laisser la structure grandir au fil du travail. Le parcours Clean Go Code de LevelUpGo couvre la conception idiomatique des packages et la structure de projet à travers des exercices que vous écrivez et exécutez dans le navigateur, et Go Fundamentals vous fait démarrer dès votre premier go.mod et la chaîne d'outils. Vous pouvez aussi parcourir la feuille de route interactive complète.
