Introducción: ¿por qué el código legacy en Go requiere refactorización?
\nGo (Golang) es famoso por su simplicidad, rendimiento y herramientas integradas para concurrencia. Sin embargo, cualquier proyecto acumula "deuda técnica" con el tiempo: variables globales, falta de interfaces, números mágicos, funciones ilegibles y ausencia de pruebas. A este código se le llama legacy. La refactorización no es reescribir todo desde cero, sino mejorar gradualmente la estructura sin cambiar el comportamiento externo. En este artículo, analizaremos un plan paso a paso para refactorizar código legacy en Go, desde el análisis hasta la implementación.
\n\nPaso 1: Diagnóstico del código legacy
\nAntes de cambiar algo, hay que entender con qué estamos tratando. Las principales "banderas rojas" en proyectos Go son:
\n- \n
- Variables globales (
var db *sql.DB) — dificultan las pruebas y el trabajo concurrente. \n - Funciones monstruo (más de 50-100 líneas) — hacen el código ilegible. \n
- Falta de interfaces — acoplamiento rígido a implementaciones concretas. \n
- Cadenas/números mágicos — dificultan la búsqueda y modificación. \n
- Falta de pruebas — refactorizar sin pruebas es caminar sobre hielo fino. \n
Herramientas de análisis: use go vet y staticcheck para encontrar construcciones sospechosas. También es útil gocyclo para calcular la complejidad ciclomática.
Ejemplo de código problemático:
\npackage main\n\nimport "fmt"\n\nvar config map[string]string // variable global\n\nfunc main() {\n config = map[string]string{"port": "8080"}\n startServer()\n}\n\nfunc startServer() {\n // cadena mágica "port"\n fmt.Println("Iniciando en puerto:", config["port"])\n // ... 100 líneas de código\n}\n\nPaso 2: Crear una red de seguridad con pruebas
\nSin pruebas, la refactorización se convierte en una adivinanza. Su tarea es cubrir la lógica clave con pruebas antes de los cambios. Use el enfoque de Golden Files (archivos de referencia) para salidas complejas o pruebas de integración.
\nEjemplo de agregar una prueba para una función de código legacy:
\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("Esperado %f, obtenido %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("Esperado %f, obtenido %f", expected, result)\n }\n}\nConsejo: use go test -cover para evaluar la cobertura. Intente cubrir al menos el 70% de la lógica de negocio antes de comenzar la refactorización.
Paso 3: Descomposición gradual y extracción de interfaces
\nEl objetivo principal de la refactorización es hacer que el código sea comprobable y flexible. Comience extrayendo interfaces para dependencias externas (bases de datos, clientes HTTP, sistemas de archivos).
\n3.1 Extraer una interfaz en lugar de usar la BD directamente
\nSupongamos que tenemos una función que trabaja directamente con *sql.DB:
// código legacy\nfunc GetUser(db *sql.DB, id int) (*User, error) {\n row := db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)\n // ...\n}\nRefactorización: creamos la interfaz 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