In der Welt der Softwareentwicklung begegnet man immer häufiger dem Begriff Dependency Injection, kurz DI. Gerade bei Programmiersprachen wie Go wird die Technik häufig diskutiert, oft verbunden mit Frameworks, die versprechen, den Aufwand beim Verwalten von Abhängigkeiten zu minimieren. Dabei lässt sich beobachten, dass viele Entwickler in Go eher Abstand zu solchen Frameworks halten – und das aus guten Gründen. Die Kernidee von Dependency Injection ist eigentlich denkbar einfach und elegant: anstatt dass eine Komponente ihre Abhängigkeiten selbst erzeugt, bekommt sie diese von außen übergeben. So wird sie flexibler, leichter testbar und besser wartbar.
Dennoch hat sich um das Thema DI über Jahre herum ein Dschungel aus Begrifflichkeiten, Frameworks und Konzepten entwickelt, die gerade in der Go-Welt oft überladen wirken. In Go zeigt sich, dass man Dependency Injection meist gar nicht mit einem Framework braucht. Stattdessen lässt sich die Abhängigkeitsverwaltung sehr gut manuell, explizit und typensicher im Code umsetzen, was gerade bei größeren Projekten viele Vorteile mit sich bringt. Das Herzstück von DI ist im Wesentlichen, einen Konstruktor so zu gestalten, dass er keine eigenen Abhängigkeiten innerhalb seiner Funktion erzeugt, sondern diese beim Erzeugen übergeben bekommt. Wenn beispielsweise ein Server Modul eine Datenbank benötigt, wäre der übliche Fehler, innerhalb der Server-Funktion selbst eine neue Datenbankinstanz zu erstellen.
Richtig wäre stattdessen, die Datenbank vorher anzulegen und dieser dann als Parameter in den Server-Konstruktor zu übergeben. Das macht den Server nicht nur unabhängiger von der konkreten Implementierung der Datenbank, sondern erleichtert das Testen enorm. Im Go-Universum wird dieses Prinzip oft noch konsequenter umgesetzt, indem Abhängigkeiten als Interfaces definiert sind. Das bedeutet, die Funktionalität, die vom Datenbankzugriff erwartet wird, wird in einem Interface zusammengefasst. Es gibt dann verschiedene Implementierungen: eine reale Datenbankanbindung für den produktiven Einsatz und eine Fake-Variante für Unit Tests.
Dieses Muster macht es spielend leicht, die Implementierung auszutauschen, ohne die Nutzungsstellen im Code zu ändern. Über die Typensicherheit der Sprache wird sichergestellt, dass alle Implementierungen das Interface erfüllen, was Fehler frühzeitig erkennt. Anders als viele andere Sprachen mit hochkomplexen DI-Frameworks kommt Go ohne Reflection oder Code-Generierung im DI-Kontext sehr gut aus. Frameworks wie Uber’s dig oder Google’s Wire bedienen sich nämlich genau solcher Techniken, um den Abhängigkeitsgraphen automatisch zu visualisieren und aufzulösen. Während das in sehr großen, komplexen Systemen Vorteile bringen kann, erzeugt es in der Praxis oft mehr Schwierigkeiten als es löst.
Bei dig zum Beispiel werden die Abhängigkeiten über sogenannte Provide-Funktionen registriert und der Graph erst zur Laufzeit aufgelöst. Das führt zu einem erhöhten Debugging-Aufwand, weil Fehler oft erst dann auffallen, wenn etwas tatsächlich ausgeführt wird. Die Fehlermeldungen können tief verschachtelt sein und machen das Auffinden des eigentlichen Problems komplex. Wire hingegen löst das über Code-Generierung und überprüft die Abhängigkeiten schon zur Kompilierzeit. Das mag für viele interessant sein, bedeutet aber zusätzlichen Bedienungsaufwand, da es ein eigenes Mini-Framework mit eigenen Regeln, Sets und Generator-Anweisungen benötigt.
Außerdem verliert das gesamte Team Energie und Zeit für das Erlernen dieser Konzepte, die dann auch nicht portabel sind, sobald man sich für ein anderes Werkzeug entscheidet. Die Wahrheit ist, dass Go als Sprache sehr gut das manuelle Dependency Injection unterstützt. Dies liegt daran, dass Konstruktoren ganz einfach normale Funktionen sind, Interfaces sauber definierte Implementierungen zulassen und die Kompilierung schnell genug ist, um sofortiges Feedback zu geben. Ein sauberer, expliziter Setup-Code, bei dem man die Abhängigkeiten der Komponenten in der Hauptfunktion oder in dedizierten Builder-Funktionen erzeugt und zusammenfügt, ist für viele Projekte die bessere Wahl. Klar, etwas mehr Tipperei ist damit verbunden als mit einem Framework, aber der dadurch gewonnene Überblick ist deutlich höher.
Man sieht auf einen Blick, welche Komponenten wie miteinander verbunden sind und welche Instanzen wann erzeugt werden. Die Komplexität bleibt transparent und kann über einzelne Dateien oder Funktionen sauber organisiert werden. Falls die main-Funktion mit der Zeit zu groß wird, lassen sich die Teile in eigene Builder-Funktionen auslagern, was weiterhin sehr lesbar und nachvollziehbar bleibt. Vor allem aber profitiert man von der nativen Kompilierzeitprüfung der Sprache, sodass die Parameterlisten bei Änderungen sofort Fehler anzeigen. Die eigene IDE kann typischen Komfort wie Autovervollständigung und Refactoring bestens liefern, weil keinerlei Magie im Hintergrund passiert.
In der Praxis haben viele Entwickler die Erfahrung gemacht, dass DI-Frameworks mehr Frust als Nutzen bringen. Die vielen Abstraktionen erschweren den Einstieg in den Code, die Fehlersuche wird komplexer und die Wartung leidet, wenn neue Teammitglieder die Heimlichkeiten der Framework-spezifischen APIs erst lernen müssen. Natürlich gibt es durchaus Fälle, bei denen DI-Frameworks in Go ihren Platz haben, nämlich in extrem großen, modularen Systemen, wo das automatische Zusammenfügen von hunderten Abhängigkeiten, die oft dynamisch variiert werden, die Arbeit erleichtert. Auch Teams, die schon viel Erfahrung mit einem Framework wie Fx von Uber besitzen und sich an dessen Ökosystem gewöhnt haben, profitieren von Konsistenz. Aber für die meisten Anwendungsfälle reicht es aus, sauber gemanagte, manuelle DI anzuwenden.
Hierbei wird der Fokus auf klare Verantwortung und einfache Strukturen gelegt, was letztlich zu besser wartbarem und verständlichem Code führt. Denn Go selbst bietet mit seinen Features alle Werkzeuge, die man braucht, um DI effektiv umzusetzen. Entwicklungsprozesse werden dadurch nicht verlangsamt, sondern im Gegenteil durch die reduzierte Komplexität sogar beschleunigt. Für Entwickler, die gerade von objektorientierten Sprachen kommen und den schier endlosen DI-Framework-Dschungel hinter sich lassen wollen, öffnet sich mit Go eine erfrischend andere Herangehensweise. DI bedeutet eben nicht, dass man ein Framework benutzen muss oder stundenlang konfigurieren.
Es bedeutet einfach, dass man seine Abhängigkeiten bewusst verwaltet und nicht versteckt. Dieses Prinzip zu verstehen ist das eigentliche Ziel. Zusammenfassend lässt sich sagen, dass der natürliche Stil in Go darin besteht, Dependency Injection explizit und manuell umzusetzen. Durch gut strukturierte Konstruktorfunktionen, Interfaces für Abstraktion und klaren Aufbau der Komponenten entsteht ein nachhaltiger Entwicklungsansatz, der in vielen Fällen robuster und leichter nachvollziehbar ist als das Vorgehen mit DI-Frameworks. Wenn Sie auf Nummer sicher gehen wollen, setzen Sie lieber auf die Einfachheit, Transparenz und Kompilierzeit-Sicherheit, die Go Ihnen bietet, anstatt sich auf komplexe Frameworks einzulassen.
So bleibt Ihr Projekt übersichtlicher, Ihr Team effektiver und Sie bewahren die legendäre Einfachheit, für die Go steht.