Iniezione delle Dipendenze in Go: Pattern e Best Practice
Padronare i pattern DI per codice Go testabile
Iniezione delle dipendenze (DI) è un pattern di progettazione fondamentale che promuove codice pulito, testabile e mantenibile nelle applicazioni Go.
Che tu stia costruendo API REST, implementando pattern di database multi-tenant, o lavorando con librerie ORM, comprendere l’iniezione delle dipendenze migliorerà significativamente la qualità del tuo codice.

Cos’è l’Iniezione delle Dipendenze?
L’iniezione delle dipendenze è un pattern di progettazione in cui i componenti ricevono le loro dipendenze da fonti esterne invece di crearle internamente. Questo approccio decoppia i componenti, rendendo il codice più modulare, testabile e mantenibile.
In Go, l’iniezione delle dipendenze è particolarmente potente grazie alla filosofia di progettazione basata su interfacce del linguaggio. La soddisfazione implicita delle interfacce in Go consente di scambiare implementazioni facilmente senza modificare il codice esistente.
Perché usare l’Iniezione delle Dipendenze in Go?
Miglior testabilità: Iniettando le dipendenze, puoi facilmente sostituire le implementazioni reali con mock o test double. Questo ti permette di scrivere test unitari veloci, isolati e che non richiedono servizi esterni come database o API.
Miglior manutenibilità: Le dipendenze diventano esplicite nel codice. Quando guardi una funzione costruttrice, vedi immediatamente cosa un componente richiede. Questo rende la base di codice più facile da comprendere e modificare.
Accoppiamento debole: I componenti dipendono da astrazioni (interfacce) piuttosto che da implementazioni concrete. Questo significa che puoi cambiare le implementazioni senza influenzare il codice dipendente.
Flessibilità: Puoi configurare diverse implementazioni per diversi ambienti (sviluppo, test, produzione) senza cambiare la logica di business.
Iniezione tramite Costruttore: Il Modo Go
Il modo più comune e idiomatico per implementare l’iniezione delle dipendenze in Go è tramite funzioni costruttrici. Queste sono tipicamente chiamate NewXxx e accettano le dipendenze come parametri.
Esempio Base
Ecco un esempio semplice che dimostra l’iniezione tramite costruttore:
// Definisci un'interfaccia per il repository
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// Il servizio dipende dall'interfaccia del repository
type UserService struct {
repo UserRepository
}
// Il costruttore inietta la dipendenza
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// I metodi usano la dipendenza iniettata
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
Questo pattern rende chiaro che UserService richiede un UserRepository. Non puoi creare un UserService senza fornire un repository, il che previene errori in esecuzione dovuti a dipendenze mancanti.
Dipendenze Multiple
Quando un componente ha più dipendenze, aggiungile semplicemente come parametri del costruttore:
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,
}
}
Progettazione delle Interfacce per l’Iniezione delle Dipendenze
Uno dei principi chiave nell’implementare l’iniezione delle dipendenze è il Principio di Inversione delle Dipendenze (DIP): i moduli ad alto livello non devono dipendere da quelli a basso livello; entrambi devono dipendere da astrazioni.
In Go, ciò significa definire interfacce piccole e focalizzate che rappresentano solo ciò di cui il componente ha bisogno:
// Buono: Interfaccia piccola e focalizzata
type PaymentProcessor interface {
ProcessPayment(amount float64) error
}
// Cattivo: Interfaccia grande con metodi non necessari
type PaymentService interface {
ProcessPayment(amount float64) error
RefundPayment(id string) error
GetPaymentHistory(userID int) ([]Payment, error)
UpdatePaymentMethod(userID int, method PaymentMethod) error
// ... molti altri metodi
}
L’interfaccia più piccola segue il Principio di Segregazione delle Interfacce - i client non dovrebbero dipendere da metodi che non usano. Questo rende il codice più flessibile e più facile da testare.
Esempio Reale: Astrazione del Database
Quando si lavora con i database nelle applicazioni Go, avrai spesso bisogno di astrarre le operazioni del database. Ecco come l’iniezione delle dipendenze aiuta:
// Interfaccia del database - astrazione ad alto livello
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)
}
// Il repository dipende dall'astrazione
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()
// ... parsing delle righe
}
Questo pattern è particolarmente utile quando si implementano pattern di database multi-tenant, dove potresti dover passare tra diverse implementazioni di database o strategie di connessione.
Il Pattern della Radice di Composizione
La Radice di Composizione è il punto in cui si assemblano tutte le dipendenze nel punto di ingresso dell’applicazione (tipicamente main). Questo centralizza la configurazione delle dipendenze e rende esplicito il grafo delle dipendenze.
func main() {
// Inizializza le dipendenze infrastrutturali
db := initDatabase()
logger := initLogger()
// Inizializza i repository
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)
// Inizializza i servizi con le dipendenze
emailSvc := NewEmailService(logger)
paymentSvc := NewPaymentService(logger)
userSvc := NewUserService(userRepo, logger)
orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)
// Inizializza gli handler HTTP
userHandler := NewUserHandler(userSvc)
orderHandler := NewOrderHandler(orderSvc)
// Collega le route
router := setupRouter(userHandler, orderHandler)
// Avvia il server
log.Fatal(http.ListenAndServe(":8080", router))
}
Questo approccio rende chiaro come è strutturata la tua applicazione e da dove provengono le dipendenze. È particolarmente prezioso quando si costruiscono API REST in Go, dove è necessario coordinare diversi livelli di dipendenze.
Framework per l’Iniezione delle Dipendenze
Per applicazioni più grandi con grafici di dipendenze complessi, gestire manualmente le dipendenze può diventare laborioso. Go ha diversi framework DI che possono aiutare:
Google Wire (Iniezione delle Dipendenze a Tempo di Compilazione)
Wire è uno strumento di iniezione delle dipendenze a tempo di compilazione che genera codice. È sicuro dal punto di vista dei tipi e non ha overhead in esecuzione.
Installazione:
go install github.com/google/wire/cmd/wire@latest
Esempio:
// 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 genera il codice di iniezione delle dipendenze al momento della compilazione, garantendo la sicurezza dei tipi e eliminando l’overhead della riflessione in esecuzione.
Uber Dig (Iniezione delle Dipendenze in Esecuzione)
Dig è un framework di iniezione delle dipendenze in esecuzione che utilizza la riflessione. È più flessibile ma ha un certo costo in esecuzione.
Esempio:
import "go.uber.org/dig"
func main() {
container := dig.New()
// Registra i fornitori
container.Provide(NewDB)
container.Provide(NewUserRepository)
container.Provide(NewUserService)
container.Provide(NewUserHandler)
// Invoca la funzione che ha bisogno delle dipendenze
err := container.Invoke(func(handler *UserHandler) {
// Usa l'handler
})
if err != nil {
log.Fatal(err)
}
}
Quando Usare i Framework
Usa un framework quando:
- Il tuo grafo di dipendenze è complesso con molti componenti interconnessi
- Hai più implementazioni della stessa interfaccia che devono essere selezionate in base alla configurazione
- Vuoi una risoluzione automatica delle dipendenze
- Stai costruendo un’applicazione di grandi dimensioni dove il cablaggio manuale diventa soggetto a errori
Rimani con l’iniezione manuale quando:
- La tua applicazione è di piccole o medie dimensioni
- Il grafo delle dipendenze è semplice e facile da seguire
- Vuoi mantenere le dipendenze minime ed esplicite
- Preferisci il codice esplicito rispetto a quello generato
Test con l’Iniezione delle Dipendenze
Uno dei principali vantaggi dell’iniezione delle dipendenze è la miglior testabilità. Ecco come la DI rende più facile il testing:
Esempio di Test Unitari
// Implementazione mock per il 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 usando il 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)
}
Questo test si esegue velocemente, non richiede un database e testa la logica di business in modo isolato. Quando si lavora con librerie ORM in Go, puoi iniettare repository mock per testare la logica dei servizi senza configurare il database.
Pattern e Best Practice Comuni
1. Usa la Segregazione delle Interfacce
Mantieni le interfacce piccole e focalizzate su ciò che il client ha effettivamente bisogno:
// Buono: Il client ha bisogno solo di leggere gli utenti
type UserReader interface {
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
}
// Interfaccia separata per la scrittura
type UserWriter interface {
Save(user *User) error
Delete(id int) error
}
2. Restituisci Errori dai Costruttori
I costruttori dovrebbero validare le dipendenze e restituire errori se l’inizializzazione fallisce:
func NewUserService(repo UserRepository) (*UserService, error) {
if repo == nil {
return nil, errors.New("user repository cannot be nil")
}
return &UserService{repo: repo}, nil
}
3. Usa il Context per le Dipendenze Scoperte alle Richieste
Per le dipendenze specifiche delle richieste (come le transazioni del database), passale tramite context:
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. Evita l’Iniezione Eccessiva
Non iniettare dipendenze che sono veramente dettagli di implementazione interna. Se un componente crea e gestisce i propri oggetti helper, va bene:
// Buono: L'helper interno non ha bisogno di iniezione
type UserService struct {
repo UserRepository
// Cache interna - non ha bisogno di iniezione
cache map[int]*User
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{
repo: repo,
cache: make(map[int]*User),
}
}
5. Documenta le Dipendenze
Usa commenti per documentare perché le dipendenze sono necessarie e qualsiasi vincolo:
// UserService gestisce la logica di business relativa agli utenti.
// Richiede un UserRepository per l'accesso ai dati e un Logger per
// il tracciamento degli errori. Il repository deve essere thread-safe
// se usato in contesti concorrenti.
func NewUserService(repo UserRepository, logger Logger) *UserService {
// ...
}
Quando NON Usare l’Iniezione delle Dipendenze
L’iniezione delle dipendenze è uno strumento potente, ma non è sempre necessaria:
Salta la DI per:
- Oggetti semplici o strutture dati
- Funzioni helper interne o utility
- Script one-off o utility piccole
- Quando l’istanziazione diretta è più chiara e semplice
Esempio di quando NON usare la DI:
// Struttura semplice - non serve DI
type Point struct {
X, Y float64
}
func NewPoint(x, y float64) Point {
return Point{X: x, Y: y}
}
// Utility semplice - non serve DI
func FormatCurrency(amount float64) string {
return fmt.Sprintf("€%.2f", amount)
}
Integrazione con l’Ecosistema Go
L’iniezione delle dipendenze funziona perfettamente con altri pattern e strumenti Go. Quando si costruiscono applicazioni che utilizzano la libreria standard Go per il web scraping o generando report PDF, puoi iniettare questi servizi nella logica di business:
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,
}
}
Questo ti permette di scambiare le implementazioni della generazione PDF o utilizzare mock durante i test.
Conclusione
L’iniezione delle dipendenze è un pilastro per scrivere codice Go mantenibile e testabile. Seguendo i pattern descritti in questo articolo - iniezione tramite costruttore, progettazione basata su interfacce e il pattern della radice di composizione - creerai applicazioni più facili da comprendere, testare e modificare.
Inizia con l’iniezione manuale tramite costruttore per applicazioni di piccole e medie dimensioni, e considera framework come Wire o Dig man mano che il tuo grafo di dipendenze diventa più complesso. Ricorda che l’obiettivo è la chiarezza e la testabilità, non la complessità fine a sé stessa.
Per ulteriori risorse sullo sviluppo in Go, consulta la nostra Go Cheatsheet per un riferimento rapido sulla sintassi Go e sui pattern comuni.
Link Utili
- Go Cheatsheet
- Alternative a Beautiful Soup per Go
- Generazione di PDF in Go - Librerie ed esempi
- Pattern di database multi-tenant con esempi in Go
- Quale ORM usare in Go: GORM, sqlc, Ent o Bun?
- Guida completa per la creazione di API REST in Go
Risorse Esterne
- Come usare l’Iniezione delle Dipendenze in Go - freeCodeCamp
- Best Practice per l’Inversione delle Dipendenze in Golang - Relia Software
- Guida Pratica all’Iniezione delle Dipendenze in Go - Relia Software
- Google Wire - Iniezione delle Dipendenze a Tempo di Compilazione
- Uber Dig - Iniezione delle Dipendenze in Esecuzione
- Principi SOLID in Go - Software Patterns Lexicon