Introduction : Pourquoi le code legacy en Go nécessite une approche particulière
Go (Golang) est réputé pour sa simplicité et ses performances, mais même avec lui, du code legacy apparaît avec le temps : packages monolithiques, variables globales, absence d'interfaces et de tests. Réusiner un tel code n'est pas simplement "réécrire joliment", mais une tâche stratégique dont dépendent la stabilité et la rapidité de développement.
Dans cet article, nous examinerons les techniques avancées de réusinage de code legacy en Go : de l'extraction de modules à l'injection sécurisée d'interfaces. Vous apprendrez comment transformer un "code spaghetti" en une architecture maintenable sans arrêter la production.
1. Diagnostic et cartographie du code legacy
Avant le réusinage, il est nécessaire de comprendre la structure actuelle. En Go, il existe des outils et approches intégrés pour cela :
- Analyse des dépendances : utilisez
go mod graphetgo list -jsonpour visualiser les liens entre les packages. - Recherche de variables globales :
grep -r "var " *.go— ce sont souvent elles qui sont à l'origine des problèmes. - Identification des "odeurs de code" : fonctions longues, duplication, absence de gestion des erreurs.
// Exemple : recherche de toutes les variables globales dans le projetpackage 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("Variable globale dans %s : %v", path, genDecl.Specs) } } return nil })}Important : avant le réusinage, créez un instantané complet des tests (même s'il n'y en a pas — écrivez au moins des tests de fumée pour les chemins critiques).
2. Extraction stratégique des modules : du monolithe aux packages
Le code legacy contient souvent un seul package géant main ou utils. Le réusinage avancé commence par une extraction granulaire :
- Extraction par responsabilité : divisez le code en couches (handler, service, repository).
- Création de packages internes : utilisez
internal/pour masquer l'implémentation. - Interfaces comme contrats : ne créez pas d'interfaces à l'avance — extrayez-les des implémentations concrètes.
// Avant le réusinage : tout dans un seul fichierpackage 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}
// Après le réusinage : séparation en packages// 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}Conseil : utilisez le modèle "Strangler Fig" — remplacez progressivement les anciennes fonctions par de nouvelles, sans supprimer l'ancien code immédiatement.
3. Élimination des états globaux et injection de dépendances
Les variables globales en Go sont la principale cause d'instabilité et d'impossibilité de test. La technique avancée est l'inversion de contrôle (IoC) via des constructeurs et des interfaces.
// Problème : logger et config globauxvar logger *log.Loggervar config *Config
func Init() { logger = log.New(os.Stdout, "", log.LstdFlags) config = loadConfig()}
// Solution : injection de d