L'injection de dépendances en Go : modèles et bonnes pratiques
Maîtrisez les modèles DI pour un code Go testable
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 construissiez des API REST, que vous implémentiez des modèles de base de données multi-locataires, ou que vous travailliez avec des bibliothèques ORM, comprendre 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 depuis des sources externes plutôt que de les créer internement. 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 en raison de la philosophie de conception basée sur les interfaces du langage. La satisfaction implicite des interfaces en Go signifie que vous pouvez facilement remplacer les implémentations sans modifier le code existant.
Pourquoi utiliser l’injection de dépendances en Go ?
Meilleure testabilité : En injectant les dépendances, vous pouvez facilement remplacer les implémentations réelles par des doubles ou des mocks. Cela vous permet d’écrire des tests unitaires rapides, isolés et qui ne nécessitent pas de services externes comme les bases de données ou les API.
Meilleure maintenabilité : Les dépendances deviennent explicites dans votre code. Lorsque vous regardez une fonction de constructeur, vous voyez immédiatement ce dont un composant a besoin. Cela rend le codebase plus facile à comprendre et à modifier.
Couplage faible : 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 manière Go
La façon la plus courante et idiomatique d’implémenter l’injection de dépendances en Go est via les fonctions de constructeur. 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 de runtime dues à des dépendances manquantes.
Plusieurs dépendances
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 devraient pas dépendre des modules de bas niveau ; tous devraient dépendre d’abstractions.
En Go, cela signifie définir des interfaces petites et ciblées qui représentent uniquement ce dont votre composant a besoin :
// Bon : Petite interface ciblée
type PaymentProcessor interface {
ProcessPayment(amount float64) error
}
// Mauvais : Interface grande 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 ségrégation 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 concret : Abstraction de base de données
Lorsque vous travaillez avec des bases de données dans des applications Go, vous aurez souvent besoin d’abstraire les opérations de base de données. Voici comment l’injection de dépendances vous 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()
// ... parser les lignes
}
Ce modèle est particulièrement utile lors de l’implémentation des modèles de base de données multi-locataires, où vous pourriez avoir besoin de basculer entre différentes implémentations de base de données ou stratégies de connexion.
Le patron de la racine de composition
Le patron de la racine de composition est le lieu 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 des 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 les 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 rend clair comment votre application est structurée et d’où proviennent les dépendances. Elle est particulièrement précieuse lors de la création de API REST en Go, où vous devez coordonner plusieurs couches de dépendances.
Cadres 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 dispose de plusieurs cadres de DI qui peuvent aider :
Google Wire (DI au moment de la compilation)
Wire est un outil de DI au moment de la compilation qui génère du code. Il est type-safe et n’a aucun surcoût au moment de 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 au moment de la compilation, garantissant la sécurité des types et éliminant le surcoût d’exécution lié à la réflexion.
Uber Dig (DI au moment de l’exécution)
Dig est un cadre de DI au moment de l’exécution qui utilise la réflexion. Il est plus flexible mais a un certain coût au moment de 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)
// Appeler une fonction qui nécessite des dépendances
err := container.Invoke(func(handler *UserHandler) {
// Utiliser le gestionnaire
})
if err != nil {
log.Fatal(err)
}
}
Quand utiliser des cadres
Utilisez un cadre 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 selon la configuration
- Vous souhaitez une résolution automatique des dépendances
- Vous construisez une grande application où le câblage manuel devient source d’erreurs
Restez avec l’injection manuelle lorsque :
- Votre application est petite à moyenne
- Le graphe de dépendances est simple et facile à suivre
- Vous souhaitez garder les dépendances minimales et explicites
- Vous préférez le code explicite au code généré
Test avec l’injection de dépendances
L’un des principaux avantages de l’injection de dépendances est une meilleure testabilité. Voici comment la DI rend le test plus facile :
Exemple de test unitaire
// Implémentation de test
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 avec 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, n’a pas besoin d’une base de données et teste votre logique métier en isolation. Lorsque vous travaillez avec des bibliothèques ORM en Go, vous pouvez injecter des repositories de test pour tester la logique du service sans configuration de base de données.
Modèles courants et bonnes pratiques
1. Utiliser la ségrégation des interfaces
Gardez les interfaces petites et ciblées sur ce dont le client a vraiment 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("user repository cannot be nil")
}
return &UserService{repo: repo}, nil
}
3. Utiliser le contexte pour les dépendances spécifiques à la requête
Pour les dépendances spécifiques à la requête (comme les transactions de base de données), transmettez-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
Ne pas injecter des dépendances qui sont véritablement des détails d’implémentation internes. Si un composant crée et gère ses propres objets d’aide, cela est acceptable :
// Bon : L'helper 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 toute contrainte :
// UserService gère la logique métier liée 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 si 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 :
Passer l’ID pour :
- Des objets simples ou des structures de données
- Des fonctions d’aide internes ou des utilitaires
- Des scripts à usage unique ou des utilitaires petits
- Lorsque l’instanciation directe est plus claire et plus simple
Exemple de cas où NE PAS utiliser l’ID :
// Structure simple - pas besoin d'ID
type Point struct {
X, Y float64
}
func NewPoint(x, y float64) Point {
return Point{X: x, Y: y}
}
// Utilitaire simple - pas besoin d'ID
func FormatCurrency(amount float64) string {
return fmt.Sprintf("$%.2f", amount)
}
Intégration avec l’écosystème Go
L’injection de dépendances fonctionne de manière fluide avec d’autres modèles et outils Go. Lors de la création d’applications qui utilisent 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 doubles lors des tests.
Conclusion
L’injection de dépendances est un pilier de l’écriture de 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 patron de la racine de composition — vous créerez des applications plus faciles à comprendre, à tester et à modifier.
Commencez avec l’injection manuelle par constructeur pour les applications petites à moyennes, et envisagez des cadres comme Wire ou Dig lorsque votre graphe de dépendances s’élargit. N’oubliez pas que l’objectif est la clarté et la testabilité, pas la complexité pour elle-même.
Pour plus de ressources sur le développement Go, consultez notre Feuille de calcul Go.
Liens utiles
- Feuille de calcul Go
- Alternatives à Beautiful Soup pour Go
- Génération de PDF en GO - Bibliothèques et exemples
- Modèles de base de données multi-locataires avec exemples en Go
- ORM à utiliser en GO : GORM, sqlc, Ent ou Bun ?
- Créer des API REST en Go : Guide complet
Ressources externes
- Comment utiliser l’injection de dépendances en Go - freeCodeCamp
- Meilleures pratiques pour l’inversion de dépendance en Golang - Relia Software
- Guide pratique de l’injection de dépendances en Go - Relia Software
- Google Wire - Injection de dépendances au moment de la compilation
- Uber Dig - Injection de dépendances au moment de l’exécution
- Principes SOLID en Go - Lexique des patrons logiciels