27 Punkte von GN⁺ 2025-04-24 | 4 Kommentare | Auf WhatsApp teilen
  • Die Sprache Go verbietet zyklische Referenzen zwischen Paketen strikt, was ganz natürlich zu einem hierarchischen Design (Layered Design) führt
  • Dieser Artikel erklärt die zwangsläufig entstehende Schichtenstruktur in Go-Projekten und argumentiert, dass sie auch ohne zusätzlich aufgezwungene Architektur völlig tragfähig ist
  • Für den Fall zyklischer Abhängigkeiten werden konkrete und praxistaugliche Refactoring-Strategien Schritt für Schritt vorgestellt
  • Jedes Paket ist als eigenständige, sinnvolle Funktionseinheit konzipiert und eignet sich dadurch besonders gut für Tests, Wartung und die Aufteilung in Microservices
  • Letztlich verhindert dieser Ansatz das in der Praxis häufige Problem, "man wollte eine Banane, bekam aber den ganzen Dschungel"

Der Layered-Design-Ansatz in Go

Grundprinzipien

  • Go verbietet zyklische Referenzen zwischen Paketen
  • Die Import-Beziehungen aller Go-Programme müssen einen gerichteten azyklischen Graphen (DAG) bilden
  • Diese Struktur ist keine Option, sondern eine auf Sprachebene erzwungene Designregel

Automatische Bildung von Paket-Schichten

  • Abgesehen von externen Paketen lassen sich die internen Pakete eines Projekts automatisch nach Referenztiefe schichten
  • Wie in der Abbildung unten befinden sich ganz unten zentrale Utility-Pakete wie metrics, logging und gemeinsame Datenstrukturen
  • Darüber entsteht schrittweise eine Struktur, in der höhere Pakete Funktionalität kombinieren und sich nach oben aufbauen

Eigenschaften dieses Designansatzes

  • Schichten basieren nicht auf hierarchischer Abstraktion, sondern auf der Richtung von Referenzen
  • Ein Paket kann mehrere Pakete auf niedrigeren Ebenen referenzieren
  • Auch bestehende Entwurfsansätze wie MVC oder hexagonale Architektur lassen sich auf diese Struktur "anwenden"
    → Dabei müssen die strukturellen Einschränkungen von Go jedoch zwingend berücksichtigt werden

Strategien zur Auflösung zyklischer Referenzen

Wenn zyklische Referenzen auftreten, sollte Refactoring in der folgenden Reihenfolge versucht werden:

1. Funktionalität verschieben

  • Die am meisten empfohlene Methode
  • Die Funktionalität, die den Zyklus verursacht, wird präzise analysiert und logisch an die passende Stelle verschoben
  • Wird nicht oft eingesetzt, verbessert aber die konzeptionelle Klarheit am stärksten

2. Gemeinsame Funktionalität in ein separates Paket auslagern

  • Typen oder Funktionen, die beide Seiten gemeinsam verwenden (Username usw.), werden in ein drittes Paket verschoben
  • Auch wenn das Paket zunächst klein wirkt, sollte man es konsequent auslagern
    → Mit der Zeit ist die Wahrscheinlichkeit hoch, dass dieses Paket wächst

3. Ein übergeordnetes Kompositionspaket erzeugen

  • Es wird ein drittes Paket erstellt, das die beiden zyklisch verbundenen Pakete zusammensetzt
  • Beispiel: Die beidseitige Abhängigkeit von Category und BlogPost wird in ein übergeordnetes Paket ausgelagert
    → Die unteren Pakete bleiben einfache structs, die eigentliche Funktionalität wird im oberen Paket zusammengesetzt

4. Interfaces einführen

  • Abhängigkeiten werden durch Interfaces ersetzt, die nur die tatsächlich benötigten Methoden enthalten
  • Entfernt unnötige Abhängigkeiten und verbessert die Testbarkeit
  • Bei übermäßigem Einsatz kann das Design allerdings auch unnötig komplex werden

5. Kopieren

  • Wenn die Abhängigkeit sehr klein ist, kann man sie einfach kopieren und verwenden
  • Das mag wie ein Verstoß gegen DRY wirken, hilft in der Praxis jedoch oft dabei, das Design klarer zu machen

6. In einem Paket zusammenführen

  • Wenn alle anderen Methoden nicht möglich sind, werden die beiden Pakete zusammengelegt
  • Solange das Paket nicht zu groß wird, ist das akzeptabel
    → Eine pauschale Zusammenlegung sollte jedoch vermieden und sorgfältig entschieden werden

Praktische Vorteile dieses Designansatzes

  • Jedes Paket bildet für sich eine sinnvolle Funktionseinheit und kann unabhängig getestet werden
  • Da Referenzen innerhalb des Pakets begrenzt sind, lässt sich ein einzelnes Paket verstehen, ohne die gesamte Codebasis zu kennen
  • Unbeabsichtigte globale Abhängigkeitsverflechtungen (= das Dschungelproblem) werden vermieden, und es wird dazu angeregt, nur das tatsächlich Benötigte zu verwenden
  • Auch bei der Aufteilung in Microservices lassen sich Teile leicht extrahieren
    → Die meisten Abhängigkeiten sind klar definiert

Fazit

  • Die Einschränkungen beim Paketdesign in Go sind kein lästiges Hindernis, sondern ein Mechanismus, der gutes Design fördert
  • Auch ohne besondere Architektur lässt sich allein über die Referenzstruktur zwischen Paketen ein robustes Design umsetzen
  • Eine präzise Analyse und Refactoring-Strategie für zyklische Referenzen ist nicht nur für Go, sondern auch für andere Sprachen wertvoll

4 Kommentare

 
bus710 2025-04-25

Am Anfang macht es Spaß, wenn man schnell etwas zusammenschreibt und es läuft.
Sobald man aber anfängt, Tests hinzuzufügen,
fragt man sich, warum man das damals so gemacht hat.

 
bungker 2025-04-24

„Ich wollte eine Banane, aber bekam den ganzen Dschungel mitgeliefert“ – dieser Spruch ist wirklich zu lustig.

 
iwanhae 2025-04-24

Eines der schwierigsten Dinge bei der Entwicklung mit Spring war, glaube ich, die zirkuläre Abhängigkeit..
Diese Frustration, wenn sich alles endlos gegenseitig initialisiert und dann wegen eines Speicherlecks abstürzt...

 
GN⁺ 2025-04-24
Hacker-News-Kommentare
  • Dass zyklische Abhängigkeiten nicht erlaubt sind, ist eine hervorragende Designentscheidung beim Aufbau großer Programme

    • Das erzwingt eine angemessene Trennung der Zuständigkeiten
    • Wenn zyklische Abhängigkeiten entstehen, gibt es ein Problem im Design, und der Artikel erklärt gut, wie man das löst
    • Manchmal werden zyklische Abhängigkeiten mithilfe von Funktionszeigern gelöst, die von einem anderen Paket neu definiert werden
    • Es wäre schön, wenn der Go-Compiler beim Erzeugen zyklischer Abhängigkeiten nützlichere Ausgaben liefern würde
    • Derzeit liefert er eine Liste aller Pakete, die an der Schleife beteiligt sind; diese kann ziemlich lang sein, und normalerweise hat die zuletzt geänderte Stelle das Problem verursacht
  • Großartiger Blogbeitrag

    • Auf dieser Website gibt es viele erstaunliche Beiträge, und wenn du gern etwas über funktionale Programmierung lernst, lohnt sich ein Blick
    • Link
  • Eine Bonus-Technik im Zusammenhang mit dem Rat, es in ein „drittes Paket“ zu verschieben

    • Wenn man viele Modellstrukturen generiert (SQL, Protobuf, GraphQL usw.), kann man eine klare Richtung zwischen den generierten Schichten festlegen
    • Der gesamte generierte Code wird dem Anwendungscode als „Basispaket“ bereitgestellt, das alles zusammensetzt
    • Vor der Einführung dieser Technik gab es das Problem, dass „Modelle Modelle zyklisch importieren“, aber mit der Einführung einer zusätzlichen strukturellen Schicht ist es vollständig verschwunden
  • Klingt, als würde jemand ein Buch über die strukturierte Methode von Yourdon lesen

  • Pakete können sich nicht gegenseitig zyklisch referenzieren

    • Tatsächlich ist das in Go mit go:linkname möglich
  • Erinnert an das konkrete Konzept eines Randomizers

  • Eine interessante Eigenheit von Golang ist, dass es auf Paketebene keine zyklischen Abhängigkeiten geben darf, in go.mod aber schon

    • Kurz gesagt: Das sollte man auch nicht tun
  • Eine schöne Erklärung dafür, wie Jerf über Pakete nachdenkt und wie mit zyklischen Abhängigkeiten umgegangen wird