L'injection de dépendances en Go : modèles et bonnes pratiques
Maîtrisez les modèles d’Injection de Dépendances pour un code Go testable
L’injection de dépendances (DI) est un modèle de conception fondamental qui favorise un code propre, testable et maintenable dans les applications Go.
Que vous soyez en train de construire des APIs REST, de mettre en œuvre des [modèles de bases de données multi-locataires](https://www.glukhov.org/fr/app-architecture/multitenancy/multi-tenant-database-patterns/ “Modèles de bases de données multi-locataires avec exemples en Go) ou de travailler avec des bibliothèques ORM, la compréhension de l’injection de dépendances améliorera considérablement la qualité de votre code.

Qu’est-ce que l’injection de dépendances ?
L’injection de dépendances est un modèle de conception où les composants reçoivent leurs dépendances de sources externes plutôt que de les créer en interne. Cette approche découple les composants, rendant votre code plus modulaire, testable et maintenable.
En Go, l’injection de dépendances est particulièrement puissante grâce à la philosophie de conception basée sur les interfaces du langage. La satisfaction implicite d’interfaces de Go signifie que vous pouvez facilement remplacer les implémentations sans modifier le code existant.
Pourquoi utiliser l’injection de dépendances en Go ?
Testabilité améliorée : En injectant des dépendances, vous pouvez facilement remplacer les implémentations réelles par des mocks ou des doubles de test. Cela vous permet d’écrire des tests unitaires rapides, isolés et qui ne nécessitent pas de services externes comme des bases de données ou des APIs.
Maintenabilité accrue : Les dépendances deviennent explicites dans votre code. Lorsque vous regardez une fonction constructeur, vous voyez immédiatement ce dont un composant a besoin. Cela rend la base de code plus facile à comprendre et à modifier.
Couplage lâche : Les composants dépendent d’abstractions (interfaces) plutôt que d’implémentations concrètes. Cela signifie que vous pouvez changer les implémentations sans affecter le code dépendant.
Flexibilité : Vous pouvez configurer différentes implémentations pour différents environnements (développement, test, production) sans modifier votre logique métier.
Injection par constructeur : La méthode Go
La manière la plus courante et idiomatique d’implémenter l’injection de dépendances en Go est via des fonctions constructeurs. Ces fonctions sont généralement nommées NewXxx et acceptent les dépendances en tant que paramètres.
Exemple basique
Voici un exemple simple démontrant l’injection par constructeur :
// Définir une interface pour le repository
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// Le service dépend de l'interface du repository
type UserService struct {
repo UserRepository
}
// Le constructeur injecte la dépendance
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// Les méthodes utilisent la dépendance injectée
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
Ce modèle rend clair que UserService nécessite un UserRepository. Vous ne pouvez pas créer un UserService sans fournir un repository, ce qui empêche les erreurs d’exécution dues à des dépendances manquantes.
Dépendances multiples
Lorsqu’un composant a plusieurs dépendances, ajoutez-les simplement comme paramètres du constructeur :
type EmailService interface {
Send(to, subject, body string) error
}
type Logger interface {
Info(msg string)
Error(msg string, err error)
}
type OrderService struct {
repo OrderRepository
emailSvc EmailService
logger Logger
paymentSvc PaymentService
}
func NewOrderService(
repo OrderRepository,
emailSvc EmailService,
logger Logger,
paymentSvc PaymentService,
) *OrderService {
return &OrderService{
repo: repo,
emailSvc: emailSvc,
logger: logger,
paymentSvc: paymentSvc,
}
}
Conception d’interfaces pour l’injection de dépendances
L’un des principes clés lors de l’implémentation de l’injection de dépendances est le Principe d’Inversion des Dépendances (DIP) : les modules de haut niveau ne doivent pas dépendre de modules de bas niveau ; les deux doivent dépendre d’abstractions.
En Go, cela signifie définir des interfaces petites et ciblées qui ne représentent que ce dont votre composant a besoin :
// Bon : Interface petite et ciblée
type PaymentProcessor interface {
ProcessPayment(amount float64) error
}
// Mauvais : Grande interface avec des méthodes inutiles
type PaymentService interface {
ProcessPayment(amount float64) error
RefundPayment(id string) error
GetPaymentHistory(userID int) ([]Payment, error)
UpdatePaymentMethod(userID int, method PaymentMethod) error
// ... beaucoup plus de méthodes
}
L’interface plus petite suit le Principe de Segmentation des Interfaces — les clients ne devraient pas dépendre de méthodes qu’ils n’utilisent pas. Cela rend votre code plus flexible et plus facile à tester.
Exemple du monde réel : Abstraction de base de données
Lors du travail avec des bases de données dans les applications Go, vous aurez souvent besoin d’abstraire les opérations de base de données. Voici comment l’injection de dépendances aide :
// Interface de base de données - abstraction de haut niveau
type DB interface {
Query(ctx context.Context, query string, args ...interface{}) (Rows, error)
Exec(ctx context.Context, query string, args ...interface{}) (Result, error)
BeginTx(ctx context.Context) (Tx, error)
}
// Le repository dépend de l'abstraction
type UserRepository struct {
db DB
}
func NewUserRepository(db DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) FindByID(ctx context.Context, id int) (*User, error) {
query := "SELECT id, name, email FROM users WHERE id = $1"
rows, err := r.db.Query(ctx, query, id)
if err != nil {
return nil, err
}
defer rows.Close()
// ... analyser les lignes
}
Ce modèle est particulièrement utile lors de l’implémentation de modèles de bases de données multi-locataires, où vous pourriez avoir besoin de passer d’une implémentation de base de données à une autre ou de stratégies de connexion.
Le modèle Composition Root
Le Composition Root (ou Racine de Composition) est l’endroit où vous assemblez toutes vos dépendances au point d’entrée de l’application (généralement main). Cela centralise la configuration des dépendances et rend le graphe de dépendances explicite.
func main() {
// Initialiser les dépendances d'infrastructure
db := initDatabase()
logger := initLogger()
// Initialiser les repositories
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)
// Initialiser les services avec leurs dépendances
emailSvc := NewEmailService(logger)
paymentSvc := NewPaymentService(logger)
userSvc := NewUserService(userRepo, logger)
orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)
// Initialiser les gestionnaires HTTP
userHandler := NewUserHandler(userSvc)
orderHandler := NewOrderHandler(orderSvc)
// Configurer les routes
router := setupRouter(userHandler, orderHandler)
// Démarrer le serveur
log.Fatal(http.ListenAndServe(":8080", router))
}
Cette approche clarifie la structure de votre application et l’origine des dépendances. Elle est particulièrement précieuse lors de la construction d’APIs REST en Go, où vous devez coordonner plusieurs couches de dépendances.
Frameworks d’injection de dépendances
Pour les applications plus grandes avec des graphes de dépendances complexes, la gestion manuelle des dépendances peut devenir fastidieuse. Go possède plusieurs frameworks DI qui peuvent aider :
Google Wire (DI à la compilation)
Wire est un outil d’injection de dépendances à la compilation qui génère du code. Il est sûr au niveau des types et n’a pas de surcharge à l’exécution.
Installation :
go install github.com/google/wire/cmd/wire@latest
Exemple :
// wire.go
//go:build wireinject
// +build wireinject
package main
import "github.com/google/wire"
func InitializeApp() (*App, error) {
wire.Build(
NewDB,
NewUserRepository,
NewUserService,
NewUserHandler,
NewApp,
)
return &App{}, nil
}
Wire génère le code d’injection de dépendances à la compilation, garantissant la sécurité des types et éliminant la surcharge de réflexion à l’exécution.
Uber Dig (DI à l’exécution)
Dig est un framework d’injection de dépendances à l’exécution qui utilise la réflexion. Il est plus flexible mais a un certain coût à l’exécution.
Exemple :
import "go.uber.org/dig"
func main() {
container := dig.New()
// Enregistrer les fournisseurs
container.Provide(NewDB)
container.Provide(NewUserRepository)
container.Provide(NewUserService)
container.Provide(NewUserHandler)
// Invoker la fonction qui a besoin de dépendances
err := container.Invoke(func(handler *UserHandler) {
// Utiliser le handler
})
if err != nil {
log.Fatal(err)
}
}
Quand utiliser des frameworks
Utilisez un framework lorsque :
- Votre graphe de dépendances est complexe avec de nombreux composants interdépendants
- Vous avez plusieurs implémentations de la même interface qui doivent être sélectionnées en fonction de la configuration
- Vous voulez une résolution automatique des dépendances
- Vous construisez une grande application où le câblage manuel devient sujet aux erreurs
Restez avec l’injection manuelle lorsque :
- Votre application est de petite à moyenne taille
- Le graphe de dépendances est simple et facile à suivre
- Vous voulez garder les dépendances minimales et explicites
- Vous préférez le code explicite au code généré
Tester avec l’injection de dépendances
L’un des principaux avantages de l’injection de dépendances est l’amélioration de la testabilité. Voici comment le DI facilite les tests :
Exemple de test unitaire
// Implémentation mock pour les tests
type mockUserRepository struct {
users map[int]*User
err error
}
func (m *mockUserRepository) FindByID(id int) (*User, error) {
if m.err != nil {
return nil, m.err
}
return m.users[id], nil
}
func (m *mockUserRepository) Save(user *User) error {
if m.err != nil {
return m.err
}
m.users[user.ID] = user
return nil
}
// Test utilisant le mock
func TestUserService_GetUser(t *testing.T) {
mockRepo := &mockUserRepository{
users: map[int]*User{
1: {ID: 1, Name: "John", Email: "john@example.com"},
},
}
service := NewUserService(mockRepo)
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
}
Ce test s’exécute rapidement, ne nécessite pas de base de données et teste votre logique métier de manière isolée. Lors du travail avec des bibliothèques ORM en Go, vous pouvez injecter des repositories mock pour tester la logique des services sans configuration de base de données.
Modèles courants et bonnes pratiques
1. Utiliser la segmentation des interfaces
Gardez les interfaces petites et ciblées sur ce dont le client a réellement besoin :
// Bon : Le client n'a besoin que de lire les utilisateurs
type UserReader interface {
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
}
// Interface séparée pour l'écriture
type UserWriter interface {
Save(user *User) error
Delete(id int) error
}
2. Retourner des erreurs depuis les constructeurs
Les constructeurs doivent valider les dépendances et retourner des erreurs si l’initialisation échoue :
func NewUserService(repo UserRepository) (*UserService, error) {
if repo == nil {
return nil, errors.New("le repository utilisateur ne peut pas être nil")
}
return &UserService{repo: repo}, nil
}
3. Utiliser le contexte pour les dépendances liées à la requête
Pour les dépendances spécifiques à une requête (comme les transactions de base de données), passez-les via le contexte :
type ctxKey string
const dbKey ctxKey = "db"
func WithDB(ctx context.Context, db DB) context.Context {
return context.WithValue(ctx, dbKey, db)
}
func DBFromContext(ctx context.Context) (DB, bool) {
db, ok := ctx.Value(dbKey).(DB)
return db, ok
}
4. Éviter l’injection excessive
N’injectez pas de dépendances qui sont de véritables détails d’implémentation internes. Si un composant crée et gère ses propres objets utilitaires, c’est acceptable :
// Bon : L'assistant interne n'a pas besoin d'injection
type UserService struct {
repo UserRepository
// Cache interne - n'a pas besoin d'injection
cache map[int]*User
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{
repo: repo,
cache: make(map[int]*User),
}
}
5. Documenter les dépendances
Utilisez des commentaires pour documenter pourquoi les dépendances sont nécessaires et toutes contraintes :
// UserService gère la logique métier relative aux utilisateurs.
// Il nécessite un UserRepository pour l'accès aux données et un Logger pour
// le suivi des erreurs. Le repository doit être thread-safe s'il est utilisé
// dans des contextes concurrents.
func NewUserService(repo UserRepository, logger Logger) *UserService {
// ...
}
Quand NE PAS utiliser l’injection de dépendances
L’injection de dépendances est un outil puissant, mais elle n’est pas toujours nécessaire :
Sautez l’injection de dépendances pour :
- Les objets valeur simples ou structures de données
- Les fonctions utilitaires internes ou utilitaires
- Les scripts ponctuels ou petits utilitaires
- Lorsque l’instanciation directe est plus claire et plus simple
Exemple de quand NE PAS utiliser l’injection de dépendances :
// Structure simple - pas besoin d'injection de dépendances
type Point struct {
X, Y float64
}
func NewPoint(x, y float64) Point {
return Point{X: x, Y: y}
}
// Utilitaire simple - pas besoin d'injection de dépendances
func FormatCurrency(amount float64) string {
return fmt.Sprintf("$%.2f", amount)
}
Intégration avec l’écosystème Go
L’injection de dépendances fonctionne parfaitement avec d’autres modèles et outils Go. Lors de la construction d’applications utilisant la bibliothèque standard Go pour le web scraping ou la génération de rapports PDF, vous pouvez injecter ces services dans votre logique métier :
type PDFGenerator interface {
GenerateReport(data ReportData) ([]byte, error)
}
type ReportService struct {
pdfGen PDFGenerator
repo ReportRepository
}
func NewReportService(pdfGen PDFGenerator, repo ReportRepository) *ReportService {
return &ReportService{
pdfGen: pdfGen,
repo: repo,
}
}
Cela vous permet de remplacer les implémentations de génération de PDF ou d’utiliser des mocks lors des tests.
Conclusion
L’injection de dépendances est un pilier pour écrire du code Go maintenable et testable. En suivant les modèles décrits dans cet article — injection par constructeur, conception basée sur les interfaces et le modèle Composition Root — vous créerez des applications plus faciles à comprendre, tester et modifier.
Commencez par l’injection de dépendances manuelle via les constructeurs pour les applications de petite à moyenne taille, et envisagez des frameworks comme Wire ou Dig à mesure que votre graphe de dépendances se complexifie. N’oubliez pas que l’objectif est la clarté et la testabilité, pas la complexité pour elle-même. Si vous structurez votre application autour de gestionnaires de commandes et de requêtes, Implémentation de CQRS en Go montre comment la même approche d’injection par constructeur câble proprement et sans cérémonies les structures Application, Commands et Queries.
Pour plus de ressources de développement Go, consultez notre Aide-mémoire Go pour une référence rapide sur la syntaxe Go et les modèles courants.
Liens utiles
- Aide-mémoire Go
- Alternatives à Beautiful Soup pour Go
- Génération de rapports PDF en Go - Bibliothèques et exemples
- Modèles de bases de données multi-locataires avec exemples en Go
- Quel ORM utiliser en Go : GORM, sqlc, Ent ou Bun ?
- Construction d’APIs REST en Go : Guide complet
Ressources externes
- Comment utiliser l’injection de dépendances en Go - freeCodeCamp
- Bonnes pratiques pour l’inversion des dépendances en Golang - Relia Software
- Guide pratique de l’injection de dépendances en Go - Relia Software
- Google Wire - Injection de dépendances à la compilation
- Uber Dig - Injection de dépendances à l’exécution
- Principes SOLID en Go - Software Patterns Lexicon
- Centre d’architecture d’applications — Conception d’API, structure de code et modèles d’intégration