Introduction : Pourquoi le code legacy en Go nécessite-t-il un réusinage ?
Go (Golang) est réputé pour sa simplicité et ses performances. Cependant, même dans les projets en Go, un code legacy s'accumule avec le temps — constructions obsolètes, fonctions illisibles, nombres "magiques" et absence de tests. Travailler avec un tel code devient un cauchemar : chaque modification est risquée, et l'ajout de nouvelles fonctionnalités prend des semaines.
Le réusinage ne consiste pas à réécrire le projet de zéro. C'est une réorganisation sécurisée de la structure interne du code sans modifier son comportement externe. Dans cet article, nous détaillerons un plan étape par étape pour le réusinage de code legacy en Go, de l'analyse à la mise en œuvre. Vous apprendrez à transformer un "code spaghetti" confus en un projet propre, maintenable et testable.
Étape 1 : Audit et cartographie du code legacy
Avant de modifier le code, vous devez comprendre à quoi vous avez affaire. Sautez cette étape — et vous risquez de casser une logique que personne ne se souvient.
1.1. Recherche des "drapeaux rouges"
Commencez par rechercher les anti-patrons typiques en Go :
- Fonctions God — fonctions de plus de 100 lignes.
- Variables globales — fort couplage et complexité de test.
- Nombres/chaînes magiques — constantes dispersées dans le code.
- Absence de gestion des erreurs — ignorance de la valeur
errorretournée. - Panique au lieu d'erreurs — utilisation de
panic/recoverpour la logique métier. - Mélange des couches — requêtes SQL à l'intérieur des gestionnaires HTTP.
1.2. Création d'une carte des dépendances
Utilisez des outils comme go callgraph ou guru pour visualiser les appels de fonctions. Cela aidera à identifier :
- Les dépendances cycliques entre les paquets.
- Le code "mort" qui n'est appelé nulle part.
- Les chemins critiques qui ne doivent pas être touchés sans une couverture de tests préalable.
Étape 2 : Création d'un "filet de sécurité" de tests
Le réusinage sans tests est un saut sans assurance. Votre tâche est d'écrire des tests caractéristiques qui figent le comportement actuel du système, même s'il semble incorrect.
2.1. Tests au niveau des fonctions
Commencez par les parties les plus critiques et isolées du code. Exemple pour une fonction que vous prévoyez de réusiner :
// Code legacyfunc CalculatePrice(base float64, discountCode string) float64 { if discountCode == "SALE10" { return base * 0.9 } if discountCode == "VIP20" { return base * 0.8 } return base}
// Test caractéristiquefunc TestCalculatePrice(t *testing.T) { tests := []struct { name string base float64 code string want float64 }{ {"sans réduction", 100.0, "", 100.0}, {"réduction 10%", 100.0, "SALE10", 90.0}, {"réduction 20%", 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. Tests d'intégration avec les Golden Files
Pour les structures complexes (réponses JSON, génération de rapports), utilisez la technique des "fichiers dorés". Enregistrez la sortie actuelle dans un fichier, et lors du réusinage, comparez-la avec celui-ci.
func TestHandler(t *testing.T) { // Exécuter la requête req := httptest.NewRequest("GET", "/api/data", nil) rec := httptest.NewRecorder() myHandler(rec, req)
// Comparer avec la référence expected, _ := os.ReadFile("testdata/golden.json") if !bytes.Equal(rec.Body.Bytes(), expected) { t.Errorf("Output mismatch") }\