Einführung: Warum Dependency Injection in Go wichtig ist
Go (Golang) ist eine Sprache, die Einfachheit und Klarheit schätzt. Mit dem Wachstum eines Projekts entsteht jedoch die Notwendigkeit, Abhängigkeiten zwischen Komponenten zu verwalten. Dependency Injection (DI) ist ein Entwurfsmuster, das hilft, lose gekoppelten, testbaren und leicht wartbaren Code zu erstellen. Im Gegensatz zu Frameworks mit Reflektions-Magie (wie Spring in Java) bietet Go einen pragmatischeren Ansatz: die Übergabe von Abhängigkeiten über Interfaces und Konstruktoren.
In diesem Artikel werden wir untersuchen, wie man DI in Go ohne externe Bibliotheken implementiert, unter Verwendung von reinen Interfaces, und wir werden auch beliebte DI-Container betrachten. Sie werden erfahren, wie Dependency Injection beim Schreiben von Unit-Tests und beim Befolgen der SOLID-Prinzipien hilft.
Grundlagen: Interfaces und Injektion über den Konstruktor
Go hat keine Klassen, aber es gibt Interfaces – das sind Verhaltensverträge. Sie sind die Grundlage für DI. Anstatt eine konkrete Implementierung innerhalb einer Struktur zu erstellen, übergeben wir sie von außen.
Beispiel ohne DI (feste Kopplung)
package main
import "fmt"
// Logger — konkrete Implementierungtype Logger struct{}
func (l *Logger) Log(msg string) { fmt.Println("LOG:", msg)}
// Service — hängt fest von Logger abtype Service struct { logger *Logger}
func NewService() *Service { return &Service{logger: &Logger{}}}
func (s *Service) DoWork() { s.logger.Log("Arbeit erledigt")}
func main() { svc := NewService() svc.DoWork()}Problem: Service kann nicht getestet werden, ohne einen echten Logger zu erstellen. Es ist unmöglich, ihn durch einen Mock oder einen anderen Logger zu ersetzen.
Beispiel mit DI über ein Interface
package main
import "fmt"
// LogInterface — Abstraktion für das Loggingtype LogInterface interface { Log(msg string)}
// ConsoleLogger — Implementierungtype ConsoleLogger struct{}
func (l *ConsoleLogger) Log(msg string) { fmt.Println("LOG:", msg)}
// Service — hängt jetzt vom Interface abtype Service struct { logger LogInterface}
// NewService — Konstruktor mit Dependency Injectionfunc NewService(logger LogInterface) *Service { return &Service{logger: logger}}
func (s *Service) DoWork() { s.logger.Log("Arbeit erledigt")}
func main() { logger := &ConsoleLogger{} svc := NewService(logger) svc.DoWork()}Jetzt arbeitet Service mit jedem Objekt, das LogInterface implementiert. Dies vereinfacht das Testen: Wir können einen Mock (Attrappe) übergeben.
Fortgeschrittene Technik: DI-Container in Go
Wenn ein Projekt wächst, wird das manuelle Übergeben von Abhängigkeiten mühsam. Hier kommen DI-Container ins Spiel – Bibliotheken, die den Abhängigkeitsgraphen automatisch auflösen. In Go sind google/wire (von Google) und uber-go/dig (von Uber) beliebt.
Beispiel mit google/wire
Wire verwendet Codegenerierung, was Reflektion zur Laufzeit ausschließt – das entspricht dem Geist von Go.
// wire.go//go:build wireinject// +build wireinject
package main
import "github.com/google/wire"
func InitializeService() *Service { wire.Build(NewService, NewConsoleLogger) return nil}Nach dem Ausführen von wire wird Code generiert, der ConsoleLogger erstellt und an NewService übergibt.
Beispiel mit uber-go/dig
Dig arbeitet über Reflektion, ist aber sehr praktisch für den dynamischen Aufbau.
package main
import ( "go.uber.org/dig")
func main() { container := dig.New()
// Konstruktoren registrieren container.Provide(NewConsoleLogger) container.Provide(NewService)
// Funktion aufrufen, die die Abhängigkeit nutzt