Golang s'est bâti une solide réputation en tant que langage rapide et efficace qui donne la priorité à la simplicité, ce qui est l'une des raisons pour lesquelles il est si couramment utilisé pour les services backend, les microservices et les outils d'infrastructure. Cependant, à mesure que de plus en plus de développeurs de langages comme Java et C# passent à Go, des questions sur la mise en œuvre d'une architecture propre se posent. Pour ceux qui sont habitués à l’approche de structuration des applications basée sur les couches de Clean Architecture, il peut sembler intuitif d’appliquer les mêmes principes à Go. Cependant, comme nous le verrons, essayer de mettre en œuvre une architecture propre dans Go se retourne souvent contre vous. Au lieu de cela, nous examinerons une structure adaptée aux atouts de Go, plus simple, plus flexible et conforme à la philosophie « garder les choses simples » de Go.
L'objectif de Clean Architecture, défendu par Oncle Bob (Robert C. Martin), est de créer des logiciels modulaires, testables et faciles à étendre. Ceci est réalisé en appliquant une séparation des préoccupations entre les couches, la logique métier principale étant isolée des préoccupations externes. Bien que cela fonctionne bien dans les langages hautement orientés objet comme Java, cela introduit des frictions dans Go. Voici pourquoi :
Dans Go, l'accent est mis sur la lisibilité, la simplicité et la réduction des frais généraux. Clean Architecture introduit des couches et des couches d'abstractions : interfaces, inversion de dépendances, injection de dépendances complexes et couches de services pour la logique métier. Cependant, ces couches supplémentaires ont tendance à ajouter une complexité inutile lorsqu'elles sont mises en œuvre dans Go.
Prenons Kubernetes comme exemple. Kubernetes est un projet massif construit en Go, mais il ne repose pas sur les principes d'une architecture propre. Au lieu de cela, il adopte une structure plate, orientée fonction, centrée sur les packages et les sous-systèmes. Vous pouvez le voir dans le référentiel Kubernetes GitHub, où les packages sont organisés par fonctionnalités plutôt que par couches rigides. En regroupant le code en fonction des fonctionnalités, Kubernetes atteint une modularité élevée sans abstractions complexes.
La philosophie Go privilégie la praticité et la rapidité. Les créateurs du langage ont toujours préconisé d’éviter une architecture excessive et de privilégier des implémentations simples. Si une abstraction n’est pas absolument nécessaire, elle n’a pas sa place dans le code Go. Les créateurs de Go ont même conçu le langage sans héritage pour éviter les pièges d'une ingénierie excessive, encourageant les développeurs à garder leurs conceptions propres et claires.
L'architecture propre s'appuie fortement sur l'injection de dépendances pour découpler les différentes couches et rendre les modules plus testables. Dans des langages comme Java, DI fait naturellement partie de l'écosystème grâce à des frameworks comme Spring. Ces frameworks gèrent automatiquement DI, vous permettant de relier facilement les dépendances, sans encombrer votre code.
Cependant, Go ne dispose pas d'un système DI natif, et la plupart des bibliothèques DI pour Go sont soit trop complexes, soit semblent peu idiomatiques. Go s'appuie sur l'injection explicite de dépendances via des fonctions de constructeur ou des paramètres de fonction, gardant les dépendances claires et évitant la « magie » cachée dans les conteneurs DI. L’approche de Go rend le code plus explicite, mais cela signifie également que si vous introduisez trop de couches, la gestion des dépendances devient ingérable et verbeuse.
Dans Kubernetes, par exemple, vous ne voyez pas de frameworks DI complexes ni de conteneurs DI. Au lieu de cela, les dépendances sont injectées de manière simple à l'aide de constructeurs. Cette conception maintient le code transparent et évite les pièges des frameworks DI. Golang encourage l'utilisation de DI uniquement là où cela a vraiment du sens, c'est pourquoi Kubernetes évite de créer des interfaces et des dépendances inutiles juste pour suivre un modèle.
Un autre défi de Clean Architecture in Go est qu'elle peut rendre les tests inutilement compliqués. En Java, par exemple, Clean Architecture prend en charge des tests unitaires robustes avec une utilisation intensive de simulations pour les dépendances. Mocking vous permet d'isoler chaque couche et de la tester indépendamment. Cependant, dans Go, la création de simulations peut s'avérer fastidieuse, et la communauté Go privilégie généralement les tests d'intégration ou les tests avec des implémentations réelles lorsque cela est possible.
Dans les projets Go de production, tels que Kubernetes, les tests ne sont pas gérés en isolant chaque composant mais en se concentrant sur l'intégration et les tests de bout en bout qui couvrent des scénarios réels. En réduisant les couches d'abstraction, les projets Go comme Kubernetes atteignent une couverture de tests élevée tout en gardant les tests proches du comportement réel, ce qui se traduit par plus de confiance lors du déploiement en production.
Donc, si Clean Architecture ne s’intègre pas bien avec Go, qu’est-ce qui le fait ? La réponse réside dans une structure plus simple et plus fonctionnelle qui met l'accent sur les packages et se concentre sur la modularité plutôt que sur une superposition stricte. Un modèle architectural efficace pour Go est basé sur l'architecture hexagonale, souvent connue sous le nom de Ports et adaptateurs. Cette architecture permet la modularité et la flexibilité sans superposition excessive.
La mise en page du projet Golang Standards est un excellent point de départ pour créer des projets prêts pour la production dans Go. Cette structure fournit une base pour organiser le code par objectif et fonctionnalité plutôt que par couche architecturale.
Vous avez tout à fait raison ! La structuration des projets Go avec une approche centrée sur les packages, où les fonctionnalités sont décomposées par packages plutôt que par une structure de dossiers en couches, s'aligne mieux sur les principes de conception de Go. Au lieu de créer des répertoires de niveau supérieur par couches (par exemple, contrôleurs, services, référentiels), il est plus idiomatique dans Go de créer des packages cohérents, chacun encapsulant ses propres modèles, services et référentiels. Cette approche basée sur des packages réduit le couplage et maintient le code modulaire, ce qui est essentiel pour une application Go de production.
Regardons une structure raffinée, centrée sur les packages, adaptée à Go :
/myapp /cmd // Entrypoints for different executables (e.g., main.go) /myapp-api main.go // Entrypoint for the main application /config // Configuration files and setup /internal // Private/internal packages (not accessible externally) /user // Package focused on user-related functionality models.go // Data models and structs specific to user functionality service.go // Core business logic for user operations repository.go // Database access methods for user data /order // Package for order-related logic models.go // Data models for orders service.go // Core order-related logic repository.go // Database access for orders /pkg // Shared, reusable packages across the application /auth // Authorization and authentication package /logger // Custom logging utilities /api // Package with REST or gRPC handlers /v1 user_handler.go // Handler for user-related endpoints order_handler.go // Handler for order-related endpoints /utils // General-purpose utility functions and helpers go.mod // Module file
Ce dossier est l'emplacement conventionnel des points d'entrée de l'application. Chaque sous-dossier représente ici un exécutable différent pour l'application. Par exemple, dans les architectures de microservices, chaque service peut avoir ici son propre répertoire avec son main.go. Le code ici doit être minimal, responsable uniquement du démarrage et de la configuration des dépendances.
Stocke les fichiers de configuration et la logique de configuration, comme le chargement de variables d'environnement ou de configuration externe. Ce package peut également définir des structures pour la configuration des applications.
C'est là que réside la logique de base de l'application, divisée en packages basés sur les fonctionnalités. Go restreint l'accès aux packages internes à partir de modules externes, gardant ces packages privés pour l'application. Chaque package (par exemple, utilisateur, commande) est autonome, avec ses propres modèles, services et référentiels. C’est la clé de la philosophie d’encapsulation de Go sans superposition excessive.
/internal/user – Gère toutes les fonctionnalités liées à l'utilisateur, y compris les modèles (structures de données), le service (logique métier) et le référentiel (interaction avec la base de données). Cela permet de conserver la logique liée à l'utilisateur dans un seul package, ce qui la rend facile à maintenir.
/internal/order – De même, ce package encapsule le code lié à la commande. Chaque domaine fonctionnel possède ses propres modèles, services et référentiels.
pkg contient des composants réutilisables qui sont utilisés dans toute l'application mais ne sont spécifiques à aucun package. Les bibliothèques ou utilitaires qui pourraient être utilisés indépendamment, tels que auth pour l'authentification ou logger pour la journalisation personnalisée, sont conservés ici. Si ces packages sont particulièrement utiles, ils peuvent également être extraits ultérieurement dans leurs propres modules.
Le package API sert de couche pour les gestionnaires HTTP ou gRPC. Les gestionnaires gèrent ici les demandes entrantes, invoquent des services et renvoient des réponses. Le regroupement des gestionnaires par version de l'API (par exemple, v1) est une bonne pratique pour la gestion des versions et permet de garder les modifications futures isolées.
Utilitaires à usage général qui ne sont liés à aucun package spécifique mais servent un objectif transversal dans la base de code (par exemple, analyse de date, manipulation de chaînes). Il est utile de garder cela minimal et de se concentrer sur des fonctions purement utilitaires.
Pour illustrer la structure, voici un aperçu plus détaillé de ce à quoi pourrait ressembler le package utilisateur :
/myapp /cmd // Entrypoints for different executables (e.g., main.go) /myapp-api main.go // Entrypoint for the main application /config // Configuration files and setup /internal // Private/internal packages (not accessible externally) /user // Package focused on user-related functionality models.go // Data models and structs specific to user functionality service.go // Core business logic for user operations repository.go // Database access methods for user data /order // Package for order-related logic models.go // Data models for orders service.go // Core order-related logic repository.go // Database access for orders /pkg // Shared, reusable packages across the application /auth // Authorization and authentication package /logger // Custom logging utilities /api // Package with REST or gRPC handlers /v1 user_handler.go // Handler for user-related endpoints order_handler.go // Handler for order-related endpoints /utils // General-purpose utility functions and helpers go.mod // Module file
// models.go - Defines the data structures related to users package user type User struct { ID int Name string Email string Password string }
// service.go - Contains the core business logic for user operations package user type UserService struct { repo UserRepository } // NewUserService creates a new instance of UserService func NewUserService(repo UserRepository) *UserService { return &UserService{repo: repo} } func (s *UserService) RegisterUser(name, email, password string) error { // Business logic for registering a user newUser := User{Name: name, Email: email, Password: password} return s.repo.Save(newUser) }
Cette structure correspond bien aux idiomes de Go :
En organisant les packages en fonction des fonctionnalités, le code est naturellement encapsulé et modulaire. Chaque package possède ses modèles, services et référentiels, gardant le code cohérent et hautement modulaire. Cela facilite la navigation, la compréhension et le test des packages individuels.
Les interfaces ne sont utilisées qu'aux limites du package (par exemple, UserRepository), là où elles ont le plus de sens pour les tests et la flexibilité. Cette approche réduit l'encombrement des interfaces inutiles, ce qui peut rendre le code Go plus difficile à maintenir.
Les dépendances sont injectées via des fonctions constructeur (par exemple, NewUserService). Cela maintient les dépendances explicites et évite le besoin de cadres d'injection de dépendances complexes, restant fidèle à la conception axée sur la simplicité de Go.
Les composants tels que auth et logger dans le répertoire pkg peuvent être partagés entre les packages, favorisant ainsi la réutilisation sans couplage excessif.
En regroupant les gestionnaires sous /api, il est facile de faire évoluer la couche API et d'ajouter de nouvelles versions ou de nouveaux gestionnaires à mesure que l'application se développe. Chaque gestionnaire peut se concentrer sur le traitement des demandes et la coordination avec les services, en gardant le code modulaire et propre.
Cette structure centrée sur les packages vous permet d'évoluer à mesure que vous ajoutez d'autres domaines (par exemple, produit, inventaire), chacun avec ses propres modèles, services et référentiels. La séparation par domaine s'aligne sur la manière idiomatique de Go d'organiser le code, en restant fidèle à la simplicité et à la clarté sur une superposition rigide.
D'après mon expérience de travail avec Go, Clean Architecture complique souvent la base de code sans ajouter de valeur significative. L'architecture propre a tendance à avoir du sens lors de la création de grandes applications d'entreprise dans des langages comme Java, où il existe une large prise en charge intégrée de DI, et où la gestion de structures d'héritage approfondies est un besoin courant. Cependant, le minimalisme de Go, son état d’esprit axé sur la simplicité et son approche simple de la concurrence et de la gestion des erreurs créent un écosystème complètement différent.
Si vous venez d'un environnement Java, il peut être tentant d'appliquer une architecture propre à Go. Cependant, les atouts de Go résident dans la simplicité, la transparence et la modularité sans trop d’abstraction. Une architecture idéale pour Go donne la priorité aux packages organisés par fonctionnalités, interfaces minimales, DI explicite, tests réalistes et adaptateurs pour plus de flexibilité.
Lors de la conception d'un projet Go, regardez des exemples concrets tels que Kubernetes, Vault et Golang Standards Project Layout. Ceux-ci montrent à quel point Go peut être puissant lorsque l’architecture privilégie la simplicité plutôt que la structure rigide. Plutôt que d’essayer d’adapter Go à un moule d’architecture propre, adoptez une architecture aussi simple et efficace que Go lui-même. De cette façon, vous créez une base de code non seulement idiomatique, mais aussi plus facile à comprendre, à maintenir et à faire évoluer.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!