Einleitung: Warum Legacy-Code in Go einen besonderen Ansatz erfordert
Go (Golang) ist bekannt für seine Einfachheit und Leistungsfähigkeit, aber selbst hier entsteht mit der Zeit Legacy-Code: monolithische Pakete, globale Variablen, fehlende Interfaces und Tests. Das Refactoring solchen Codes ist nicht einfach nur "schön umschreiben", sondern eine strategische Aufgabe, von der die Stabilität und Entwicklungsgeschwindigkeit abhängen.
In diesem Artikel betrachten wir fortgeschrittene Techniken zum Refactoring von Legacy-Code in Go: von der Modulauslagerung bis zur sicheren Einführung von Interfaces. Sie erfahren, wie Sie "Spaghetti-Code" in eine wartbare Architektur verwandeln, ohne die Produktion zu stoppen.
1. Diagnose und Kartierung von Legacy-Code
Vor dem Refactoring muss die aktuelle Struktur verstanden werden. In Go gibt es dafür integrierte Werkzeuge und Ansätze:
- Abhängigkeitsanalyse: Verwenden Sie
go mod graphundgo list -json, um die Beziehungen zwischen Paketen zu visualisieren. - Suche nach globalen Variablen:
grep -r "var " *.go– oft sind sie die Wurzel des Problems. - Identifizierung von "Code Smells": Lange Funktionen, Duplikate, fehlende Fehlerbehandlung.
// Beispiel: Suche nach allen globalen Variablen im Projektpackage main
import ( "fmt" "go/parser" "go/token" "os" "path/filepath")
func findGlobals(root string) { filepath.Walk(root, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() || filepath.Ext(path) != ".go" { return nil } fset := token.NewFileSet() f, _ := parser.ParseFile(fset, path, nil, parser.AllErrors) for _, decl := range f.Decls { if genDecl, ok := decl.(*ast.GenDecl); ok && genDecl.Tok == token.VAR { fmt.Printf("Globale Variable in %s: %v", path, genDecl.Specs) } } return nil })}Wichtig: Erstellen Sie vor dem Refactoring eine vollständige Momentaufnahme der Tests (auch wenn es keine gibt – schreiben Sie zumindest Smoke-Tests für kritische Pfade).
2. Strategische Modulauslagerung: Vom Monolithen zu Paketen
Legacy-Code enthält oft ein einziges riesiges Paket main oder utils. Fortgeschrittenes Refactoring beginnt mit einer granularen Auslagerung:
- Auslagerung nach Verantwortung: Teilen Sie den Code in Schichten auf (Handler, Service, Repository).
- Erstellung interner Pakete: Verwenden Sie
internal/, um die Implementierung zu verbergen. - Interfaces als Verträge: Erstellen Sie Interfaces nicht im Voraus – leiten Sie sie aus konkreten Implementierungen ab.
// Vor dem Refactoring: alles in einer Dateipackage main
type User struct { ID int Name string}
func SaveUser(db *sql.DB, u User) error { _, err := db.Exec("INSERT INTO users ...", u.ID, u.Name) return err}
// Nach dem Refactoring: Aufteilung in Pakete// internal/repository/user.gopackage repository
type User struct { ID int Name string}
type UserRepository struct { db *sql.DB}
func (r *UserRepository) Save(u User) error { _, err := r.db.Exec("INSERT INTO users ...", u.ID, u.Name) return err}Tipp: Verwenden Sie das "Strangler Fig"-Muster – ersetzen Sie alte Funktionen schrittweise durch neue, ohne den alten Code sofort zu löschen.
3. Beseitigung globaler Zustände und Einführung von Abhängigkeitsinjektion
Globale Variablen in Go sind die Hauptursache für Instabilität und fehlende Testbarkeit. Eine fortgeschrittene Technik ist die Inversion of Control (IoC) durch Konstruktoren und Interfaces.
// Problem: globaler Logger und Konfigurationvar logger *log.Loggervar config *Config
func Init() { logger = log.New(os.Stdout, "", log.LstdFlags) config = loadConfig()}
// Lösung: Abhängigkeitsinjektion