Dependency Injection in Go: patronen en best practices
Befaal DI-patronen voor testbare Go-code
Dependency Injectie (DI) is een fundamenteel ontwerppatroon dat schone, testbare en onderhoudbare code bevordert in Go-toepassingen.
Of je nu REST APIs bouwt, multi-tenant databasepatronen implementeert, of werkt met ORM-bibliotheken, het begrip van dependency injectie zal de kwaliteit van je code aanzienlijk verbeteren.

Wat is Dependency Injectie?
Dependency injectie is een ontwerppatroon waarbij componenten hun afhankelijkheden vanuit externe bronnen ontvangen, in plaats van ze intern te creëren. Deze aanpak ontkoppelt componenten, waardoor je code modulair, testbaar en onderhoudbaar wordt.
In Go is dependency injectie bijzonder krachtig vanwege de op interfaces gebaseerde ontwerpfilosofie van de taal. Go’s impliciete interface-ervulling betekent dat je implementaties eenvoudig kunt verwisselen zonder bestaande code te wijzigen.
Waarom Dependency Injectie gebruiken in Go?
Verbeterde testbaarheid: Door afhankelijkheden te injecteren, kun je echte implementaties eenvoudig vervangen door mocks of testdoubles. Dit stelt je in staat om unittests te schrijven die snel geïsoleerd zijn uitgevoerd worden en geen externe diensten zoals databases of APIs vereisen.
Betere onderhoudbaarheid: Afhankelijkheden worden expliciet in je code. Wanneer je naar een constructorfunctie kijkt, zie je direct wat een component vereist. Dit maakt de codebase eenvoudiger te begrijpen en te wijzigen.
Losse koppeling: Componenten zijn afhankelijk van abstracties (interfaces) in plaats van concrete implementaties. Dit betekent dat je implementaties kunt wijzigen zonder de afhankelijke code te beïnvloeden.
Flexibiliteit: Je kunt verschillende implementaties configureren voor verschillende omgevingen (ontwikkeling, testen, productie) zonder je bedrijfslogica te wijzigen.
Constructor Injectie: De Go-wijze
De meest voorkomende en idiomatische manier om dependency injectie in Go te implementeren, is via constructorfuncties. Deze worden doorgaans NewXxx genoemd en accepteren afhankelijkheden als parameters.
Basisvoorbeeld
Hier is een eenvoudig voorbeeld dat constructorinjectie demonstreert:
// Definieer een interface voor de repository
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
}
// Service is afhankelijk van de repository-interface
type UserService struct {
repo UserRepository
}
// Constructor injecteert de afhankelijkheid
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
// Methoden gebruiken de geïnjecteerde afhankelijkheid
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
Dit patroon maakt duidelijk dat UserService een UserRepository vereist. Je kunt geen UserService maken zonder een repository te bieden, wat runtime-fouten door ontbrekende afhankelijkheden voorkomt.
Meervoudige afhankelijkheden
Wanneer een component meerdere afhankelijkheden heeft, voeg ze eenvoudig toe als constructorparameters:
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,
}
}
Interface-ontwerp voor Dependency Injectie
Een van de kernprincipes bij het implementeren van dependency injectie is het Dependency Inversion Principle (DIP): hooggeplaatste modules mogen niet afhankelijk zijn van laaggeplaatste modules; beide moeten afhankelijk zijn van abstracties.
In Go betekent dit het definiëren van kleine, gefocuste interfaces die alleen vertegenwoordigen wat je component nodig heeft:
// Goed: Kleine, gefocuste interface
type PaymentProcessor interface {
ProcessPayment(amount float64) error
}
// Slecht: Grote interface met onnodige methoden
type PaymentService interface {
ProcessPayment(amount float64) error
RefundPayment(id string) error
GetPaymentHistory(userID int) ([]Payment, error)
UpdatePaymentMethod(userID int, method PaymentMethod) error
// ... veel meer methoden
}
De kleinere interface volgt het Interface Segregation Principle—cliënten mogen niet afhankelijk zijn van methoden die ze niet gebruiken. Dit maakt je code flexibeler en eenvoudiger te testen.
Voorbeeld uit de praktijk: Database-abstractie
Bij het werken met databases in Go-toepassingen zul je vaak database-operaties moeten abstracteren. Zo helpt dependency injectie:
// Database-interface - hoog niveau abstractie
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)
}
// Repository is afhankelijk van de abstractie
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()
// ... rijen parseren
}
Dit patroon is vooral nuttig bij het implementeren van multi-tenant databasepatronen, waarbij je mogelijk moet schakelen tussen verschillende database-implementaties of verbindingstrategieën.
Het Composition Root-patroon
De Composition Root is de plek waar je al je afhankelijkheden assemblert op het ingangspunt van de applicatie (doorgaans main). Dit centraliseert de configuratie van afhankelijkheden en maakt de afhankelijkheidsgrafiek expliciet.
func main() {
// Initialiseer infrastructuurafhankelijkheden
db := initDatabase()
logger := initLogger()
// Initialiseer repositories
userRepo := NewUserRepository(db)
orderRepo := NewOrderRepository(db)
// Initialiseer services met afhankelijkheden
emailSvc := NewEmailService(logger)
paymentSvc := NewPaymentService(logger)
userSvc := NewUserService(userRepo, logger)
orderSvc := NewOrderService(orderRepo, emailSvc, logger, paymentSvc)
// Initialiseer HTTP-handlers
userHandler := NewUserHandler(userSvc)
orderHandler := NewOrderHandler(orderSvc)
// Routeer opzetten
router := setupRouter(userHandler, orderHandler)
// Start server
log.Fatal(http.ListenAndServe(":8080", router))
}
Deze aanpak maakt duidelijk hoe je applicatie is gestructureerd en waar afhankelijkheden vandaan komen. Het is bijzonder waardevol bij het bouwen van REST APIs in Go, waarbij je meerdere lagen van afhankelijkheden moet coördineren.
Dependency Injectie-frameworks
Voor grotere toepassingen met complexe afhankelijkheidsgrafieken kan het handmatig beheren van afhankelijkheden omslachtig worden. Go heeft verschillende DI-frameworks die kunnen helpen:
Google Wire (Compile-time DI)
Wire is een compile-time dependency injectie-gereedschap dat code genereert. Het is typeveilig en heeft geen runtime-overhead.
Installatie:
go install github.com/google/wire/cmd/wire@latest
Voorbeeld:
// 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 genereert de dependency injectie-code tijdens het compileren, wat typeveiligheid garandeert en runtime-reflection overhead elimineert.
Uber Dig (Runtime DI)
Dig is een runtime dependency injectie-framework dat reflectie gebruikt. Het is flexibeler maar heeft een bepaalde runtime-kost.
Voorbeeld:
import "go.uber.org/dig"
func main() {
container := dig.New()
// Registreer providers
container.Provide(NewDB)
container.Provide(NewUserRepository)
container.Provide(NewUserService)
container.Provide(NewUserHandler)
// Roep functie aan die afhankelijkheden nodig heeft
err := container.Invoke(func(handler *UserHandler) {
// Gebruik handler
})
if err != nil {
log.Fatal(err)
}
}
Wanneer frameworks te gebruiken
Gebruik een framework wanneer:
- Je afhankelijkheidsgrafiek complex is met veel onderling afhankelijke componenten
- Je meerdere implementaties van dezelfde interface hebt die moeten worden geselecteerd op basis van configuratie
- Je automatische afhankelijkheidsoplossing wilt
- Je een grote applicatie bouwt waarbij handmatig wiring foutgevoelig wordt
Blijf bij handmatige DI wanneer:
- Je applicatie klein tot middelgroot is
- De afhankelijkheidsgrafiek eenvoudig is en makkelijk te volgen
- Je afhankelijkheden minimaal en expliciet wilt houden
- Je expliciete code prefereert boven gegenereerde code
Testen met Dependency Injectie
Een van de primaire voordelen van dependency injectie is verbeterde testbaarheid. Zo maakt DI testen eenvoudiger:
Voorbeeld van Unit Testing
// Mock-implementatie voor testen
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 met de 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)
}
Deze test wordt snel uitgevoerd, vereist geen database en test je bedrijfslogica geïsoleerd. Bij het werken met ORM-bibliotheken in Go, kun je mock-repositories injecteren om servicelogica te testen zonder database-opzet.
Veelvoorkomende patronen en beste praktijken
1. Gebruik Interface Segregation
Houd interfaces klein en gefocust op wat de cliënt daadwerkelijk nodig heeft:
// Goed: Cliënt heeft alleen nodig om gebruikers te lezen
type UserReader interface {
FindByID(id int) (*User, error)
FindByEmail(email string) (*User, error)
}
// Aparte interface voor schrijven
type UserWriter interface {
Save(user *User) error
Delete(id int) error
}
2. Geef fouten terug van constructoren
Constructoren moeten afhankelijkheden valideren en fouten teruggeven als initialisering faalt:
func NewUserService(repo UserRepository) (*UserService, error) {
if repo == nil {
return nil, errors.New("user repository kan niet nil zijn")
}
return &UserService{repo: repo}, nil
}
3. Gebruik Context voor request-gescopede afhankelijkheden
Voor afhankelijkheden die specifiek zijn voor een request (zoals database-transacties), geef ze door via 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. Vermijd over-injectie
Injecteer geen afhankelijkheden die werkelijk interne implementatiedetails zijn. Als een component zijn eigen hulpprogramma’s creëert en beheert, is dat prima:
// Goed: Interne helper heeft geen injectie nodig
type UserService struct {
repo UserRepository
// Interne cache - heeft geen injectie nodig
cache map[int]*User
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{
repo: repo,
cache: make(map[int]*User),
}
}
5. Documenteer afhankelijkheden
Gebruik opmerkingen om te documenteren waarom afhankelijkheden nodig zijn en welke beperkingen er zijn:
// UserService handelt gebruikersgerelateerde bedrijfslogica af.
// Het vereist een UserRepository voor datatoegang en een Logger voor
// fouttracking. De repository moet thread-safe zijn bij gebruik
// in concurrency-contexten.
func NewUserService(repo UserRepository, logger Logger) *UserService {
// ...
}
Wanneer je Dependency Injectie NIET moet gebruiken
Dependency injectie is een krachtig hulpmiddel, maar het is niet altijd nodig:
Sla DI over voor:
- Eenvoudige value-objects of datastructuren
- Interne helperfuncties of utilities
- Eénmalige scripts of kleine utilities
- Wanneer directe instantiatie duidelijker en eenvoudiger is
Voorbeeld van wanneer je DI NIET moet gebruiken:
// Eenvoudige struct - geen DI nodig
type Point struct {
X, Y float64
}
func NewPoint(x, y float64) Point {
return Point{X: x, Y: y}
}
// Eenvoudige utility - geen DI nodig
func FormatCurrency(amount float64) string {
return fmt.Sprintf("$%.2f", amount)
}
Integratie met het Go-ecosysteem
Dependency injectie werkt naadloos samen met andere Go-patronen en tools. Bij het bouwen van applicaties die Go’s standaardbibliotheek voor web scraping of het genereren van PDF-rapporten gebruiken, kun je deze diensten injecteren in je bedrijfslogica:
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,
}
}
Dit stelt je in staat om PDF-generatie-implementaties te verwisselen of mocks te gebruiken tijdens het testen.
Conclusie
Dependency injectie is een hoeksteen van het schrijven van onderhoudbare, testbare Go-code. Door de patronen in dit artikel beschreven te volgen—constructor injectie, interface-gebaseerd ontwerp en het composition root-patroon—zou je applicaties moeten creëren die eenvoudiger te begrijpen, te testen en te wijzigen zijn.
Begin met handmatige constructorinjectie voor kleine tot middelgrote applicaties, en overweeg frameworks zoals Wire of Dig naarmate je afhankelijkheidsgrafiek groeit. Onthoud dat het doel duidelijkheid en testbaarheid is, niet complexiteit om zijn eigen wil. Als je applicatie structureert rond command- en query-handlers, toont Implementeren van CQRS in Go hoe dezelfde constructorinjectie-aanpak een Application, Commands en Queries struct schoon en zonder ceremonie wiret.
Voor meer Go-ontwikkelingsresources, bekijk onze Go Cheatsheet voor een snelle referentie naar Go-syntaxis en veelvoorkomende patronen.
Nuttige links
- Go Cheatsheet
- Beautiful Soup Alternatives voor Go
- PDF genereren in GO - Bibliotheken en voorbeelden
- Multi-Tenancy Databasepatronen met voorbeelden in Go
- Welke ORM te gebruiken in GO: GORM, sqlc, Ent of Bun?
- REST APIs in Go bouwen: Complete Gids
Externe bronnen
- Hoe Dependency Injectie in Go te gebruiken - freeCodeCamp
- Beste praktijken voor Dependency Inversion in Golang - Relia Software
- Praktische gids voor Dependency Injectie in Go - Relia Software
- Google Wire - Compile-time Dependency Injectie
- Uber Dig - Runtime Dependency Injectie
- SOLID Principes in Go - Software Patterns Lexicon
- App Architecture hub — API-design, code-structuur en integratiepatronen