Einleitung: Warum Legacy-Code in Go ein Refactoring erfordert?
Go (Golang) ist bekannt für seine Einfachheit und Leistungsfähigkeit. Doch selbst in Go-Projekten sammelt sich mit der Zeit Legacy-Code an – veraltete Konstrukte, unlesbare Funktionen, "magische" Zahlen und fehlende Tests. Die Arbeit mit solchem Code wird zum Albtraum: Jede Änderung ist riskant, und das Hinzufügen neuer Funktionalitäten dauert Wochen.
Refactoring bedeutet nicht, das Projekt von Grund auf neu zu schreiben. Es ist eine sichere Reorganisation der internen Codestruktur, ohne das externe Verhalten zu ändern. In diesem Artikel erläutern wir einen schrittweisen Plan für das Refactoring von Legacy-Code in Go, von der Analyse bis zur Implementierung. Sie erfahren, wie Sie verworrenen "Spaghetti-Code" in ein sauberes, wartbares und testbares Projekt verwandeln.
Phase 1: Audit und Kartierung des Legacy-Codes
Bevor Sie den Code ändern, müssen Sie verstehen, womit Sie es zu tun haben. Überspringen Sie diese Phase – und riskieren, die Logik zu zerstören, an die sich niemand mehr erinnert.
1.1. Suche nach "roten Flaggen"
Beginnen Sie mit der Suche nach typischen Antipatterns in Go:
- God-Funktionen – Funktionen mit mehr als 100 Zeilen.
- Globale Variablen – starke Kopplung und erschwertes Testen.
- Magische Zahlen/Zeichenketten – über den Code verstreute Konstanten.
- Fehlende Fehlerbehandlung – Ignorieren des zurückgegebenen
error. - Panik statt Fehler – Verwendung von
panic/recoverfür Geschäftslogik. - Vermischung von Schichten – SQL-Abfragen innerhalb von HTTP-Handlern.
1.2. Erstellung einer Abhängigkeitskarte
Verwenden Sie Tools wie go callgraph oder guru, um Funktionsaufrufe zu visualisieren. Dies hilft, Folgendes zu identifizieren:
- Zyklische Abhängigkeiten zwischen Paketen.
- "Toten" Code, der nirgendwo aufgerufen wird.
- Kritische Pfade, die ohne vorherige Testabdeckung nicht angefasst werden sollten.
Phase 2: Erstellen eines "Sicherheitsnetzes" aus Tests
Refactoring ohne Tests ist ein Sprung ohne Sicherung. Ihre Aufgabe ist es, charakteristische Tests zu schreiben, die das aktuelle Systemverhalten festhalten, selbst wenn es falsch erscheint.
2.1. Testen auf Funktionsebene
Beginnen Sie mit den kritischsten und isoliertesten Teilen des Codes. Beispiel für eine Funktion, die refaktoriert werden soll:
// Legacy-Codefunc CalculatePrice(base float64, discountCode string) float64 { if discountCode == "SALE10" { return base * 0.9 } if discountCode == "VIP20" { return base * 0.8 } return base}
// Charakteristischer Testfunc TestCalculatePrice(t *testing.T) { tests := []struct { name string base float64 code string want float64 }{ {"ohne Rabatt", 100.0, "", 100.0}, {"10% Rabatt", 100.0, "SALE10", 90.0}, {"20% Rabatt", 100.0, "VIP20", 80.0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := CalculatePrice(tt.base, tt.code) if got != tt.want { t.Errorf("CalculatePrice(%v, %v) = %v; want %v", tt.base, tt.code, got, tt.want) } }) }}2.2. Integrationstests mit Golden Files
Für komplexe Strukturen (JSON-Antworten, Berichtserstellung) verwenden Sie die Technik der "goldenen Dateien". Speichern Sie die aktuelle Ausgabe in einer Datei und vergleichen Sie sie beim Refactoring damit.
func TestHandler(t *testing.T) { // Anfrage ausführen req := httptest.NewRequest("GET", "/api/data", nil) rec := httptest.NewRecorder() myHandler(rec, req)
// Mit Referenz vergleichen expected, _ := os.ReadFile("testdata/golden.json") if !bytes.Equal(rec.Body.Bytes(), expected) { t.Errorf("Ausgabe stimmt nicht überein") }\