Import cycle not allowed : quand Go met un barrage sur le chemin du code
\n\nChaque développeur Go rencontre tôt ou tard cette phrase. Le compilateur interrompt brusquement la construction, affichant un message sec : import cycle not allowed. Il semble que le programme soit logique et que les packages soient liés « comme il faut ». Mais Go pense autrement.
Cette erreur n'est pas un caprice du langage, mais une protection contre le chaos. Les dépendances cycliques transforment le code en spaghetti, compliquent les tests et rendent la construction imprévisible. Dans cet article, nous analyserons pourquoi import cycle not allowed se produit, et comparerons trois approches pour y remédier : via les interfaces, via un package médiateur et via la refactorisation. Vous saurez quelle méthode choisir selon la situation.
\n\nPourquoi Go interdit-il l'importation cyclique ?
\n\nContrairement aux langages à chargement dynamique (Python, JavaScript), Go est compilé statiquement. Le compilateur construit le graphe des dépendances des packages une seule fois. Si le package A importe le package B, et que B importe A — un cycle se crée. Go ne peut pas résoudre cet ordre, donc il interrompt la compilation.
\n\nExemple concret : le package users gère les utilisateurs, le package orders gère les commandes. Dans orders, on a besoin d'informations sur l'utilisateur, et dans users, la liste des commandes. Résultat : import cycle not allowed.
Scénario typique : deux packages qui « s'entendent » trop étroitement
\n\nConsidérons une situation classique. Nous avons deux packages : users et notifier.
Package users (fichier users.go) :
package users\n\nimport "notifier"\n\ntype User struct {\n Name string\n Email string\n}\n\nfunc (u *User) SendWelcome() {\n notifier.Notify(u.Email, "Welcome!")\n}\n\n\nPackage notifier (fichier notifier.go) :
package notifier\n\nimport "users"\n\nfunc Notify(email, msg string) {\n // envoyer une notification\n}\n\nfunc GetUserEmail(user *users.User) string {\n return user.Email\n}\n\n\nLe compilateur affichera : import cycle not allowed: users -> notifier -> users. Les deux packages tentent de s'importer mutuellement. Que faire ?
Approche 1 : Interfaces — « diviser pour régner »
\n\nLa manière la plus élégante est de déplacer la dépendance au niveau de l'interface. Go aime les contrats explicites.
\n\nCréons un package contracts, où nous définissons l'interface Notifier :
// contracts/contracts.go\npackage contracts\n\ntype Notifier interface {\n Notify(email, msg string)\n}\n\n\nMaintenant, users ne dépend plus directement de notifier :
// users/users.go\npackage users\n\nimport "contracts"\n\ntype User struct {\n Name string\n Email string\n}\n\nfunc (u *User) SendWelcome(n contracts.Notifier) {\n n.Notify(u.Email, "Welcome!")\n}\n\n\nEt notifier implémente l'interface :
// notifier/notifier.go\npackage notifier\n\ntype EmailNotifier struct{}\n\nfunc (e *EmailNotifier) Notify(email, msg string) {\n // logique d'envoi\n}\n\n\nAvantages :
\n- \n
- Absence totale d'importation cyclique. \n
- Tests faciles : on peut remplacer l'implémentation. \n
- Faible couplage (loose coupling). \n
Inconvénients :
\n- \n
- Nécessite un package supplémentaire pour les interfaces. \n
- Parfois, les interfaces ne sont pas nécessaires pour des projets simples. \n
Approche 2 : Package médiateur (Mediator)
\n\nSi les interfaces semblent superflues, on peut créer un troisième package qui servira de « pont » entre les deux packages en conflit.
\n\nCréons un package bridge :
// bridge/bridge.go\npackage bridge\n\nimport (\n "notifier"\n "users"\n)\n\nfunc SendWelcomeToUser(user *users.User) {\n