Einleitung: Warum erfordert Legacy-Code in Go ein Refactoring?
\nGo (Golang) ist bekannt für seine Einfachheit, Leistungsfähigkeit und integrierte Werkzeuge für Parallelität. Allerdings sammelt jedes Projekt im Laufe der Zeit "technische Schulden" an: globale Variablen, fehlende Interfaces, magische Zahlen, unlesbare Funktionen und fehlende Tests. Solcher Code wird als Legacy bezeichnet. Refactoring bedeutet nicht, alles von Grund auf neu zu schreiben, sondern die Struktur schrittweise zu verbessern, ohne das externe Verhalten zu ändern. In diesem Artikel werden wir einen schrittweisen Plan für das Refactoring von Legacy-Code in Go erläutern, von der Analyse bis zur Implementierung.
\n\nSchritt 1: Diagnose des Legacy-Codes
\nBevor wir etwas ändern, müssen wir verstehen, womit wir es zu tun haben. Die wichtigsten "roten Flaggen" in Go-Projekten sind:
\n- \n
- Globale Variablen (
var db *sql.DB) — erschweren das Testen und die parallele Arbeit. \n - Monster-Funktionen (mehr als 50-100 Zeilen) — machen den Code unlesbar. \n
- Fehlende Interfaces — starke Kopplung an konkrete Implementierungen. \n
- Magische Zeichenfolgen/Zahlen — erschweren das Auffinden und Ändern. \n
- Fehlende Tests — Refactoring ohne Tests ist wie Gehen auf dünnem Eis. \n
Werkzeuge zur Analyse: Verwenden Sie go vet und staticcheck, um verdächtige Konstrukte zu finden. Ebenfalls nützlich ist gocyclo zur Berechnung der zyklomatischen Komplexität.
Beispiel für problematischen Code:
\npackage main\n\nimport "fmt"\n\nvar config map[string]string // globale Variable\n\nfunc main() {\n config = map[string]string{"port": "8080"}\n startServer()\n}\n\nfunc startServer() {\n // magische Zeichenfolge "port"\n fmt.Println("Starting on port:", config["port"])\n // ... 100 Zeilen Code\n}\n\nSchritt 2: Erstellen eines Sicherheitsnetzes aus Tests
\nOhne Tests wird Refactoring zum Ratespiel. Ihre Aufgabe ist es, die Kernlogik vor den Änderungen mit Tests abzudecken. Verwenden Sie den Ansatz der Golden Files (Referenzdateien) für komplexe Ausgaben oder Integrationstests.
\nBeispiel für das Hinzufügen eines Tests für eine Funktion aus Legacy-Code:
\n// legacy.go (Original)\nfunc CalculateDiscount(price float64, isPremium bool) float64 {\n if isPremium {\n return price * 0.9\n }\n return price * 0.95\n}\n\n// legacy_test.go\npackage main\n\nimport "testing"\n\nfunc TestCalculateDiscount_Premium(t *testing.T) {\n result := CalculateDiscount(100.0, true)\n expected := 90.0\n if result != expected {\n t.Errorf("Expected %f, got %f", expected, result)\n }\n}\n\nfunc TestCalculateDiscount_Regular(t *testing.T) {\n result := CalculateDiscount(100.0, false)\n expected := 95.0\n if result != expected {\n t.Errorf("Expected %f, got %f", expected, result)\n }\n}\nTipp: Verwenden Sie go test -cover, um die Abdeckung zu bewerten. Versuchen Sie, vor Beginn des Refactorings mindestens 70 % der Geschäftslogik abzudecken.
Schritt 3: Schrittweise Dekomposition und Extraktion von Interfaces
\nDas Hauptziel des Refactorings ist es, den Code testbar und flexibel zu machen. Beginnen Sie mit der Extraktion von Interfaces für externe Abhängigkeiten (Datenbanken, HTTP-Clients, Dateisysteme).
\n3.1 Extraktion eines Interfaces anstelle der direkten Verwendung der Datenbank
\nAngenommen, wir haben eine Funktion, die direkt mit *sql.DB arbeitet:
// legacy code\nfunc GetUser(db *sql.DB, id int) (*User, error) {\n row := db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)\n // ...\n}\nRefactoring: Wir erstellen ein Interface UserRepository:
type UserRepository interface {\n GetByID(id int) (*User, error)\n}\n\ntype userRepo struct {\n db *sql.DB\n}\n\nfunc (r *userRepo) GetByID(id int) (*User, error) {\n row := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)\n