Введение: Почему legacy код на Go требует рефакторинга?
Go (Golang) славится своей простотой и производительностью. Однако даже в проектах на Go со временем накапливается legacy код — устаревшие конструкции, нечитаемые функции, "магические" числа и отсутствие тестов. Работа с таким кодом превращается в кошмар: каждое изменение рискованно, а добавление новой функциональности занимает недели.
Рефакторинг — это не переписывание проекта с нуля. Это безопасная реорганизация внутренней структуры кода без изменения его внешнего поведения. В этой статье мы разберем пошаговый план рефакторинга legacy кода на Go, от анализа до внедрения. Вы узнаете, как превратить запутанный "спагетти-код" в чистый, поддерживаемый и тестируемый проект.
Этап 1: Аудит и картографирование legacy кода
Прежде чем менять код, нужно понять, с чем вы имеете дело. Пропустите этот этап — и рискуете сломать логику, которую никто не помнит.
1.1. Поиск "красных флагов"
Начните с поиска типичных антипаттернов в Go:
- God-функции — функции длиной более 100 строк.
- Глобальные переменные — сильная связанность и сложность тестирования.
- Магические числа/строки — константы, разбросанные по коду.
- Отсутствие обработки ошибок — игнорирование возвращаемого
error. - Паника вместо ошибок — использование
panic/recoverдля бизнес-логики. - Смешение слоев — SQL-запросы внутри HTTP-обработчиков.
1.2. Создание карты зависимостей
Используйте инструменты вроде go callgraph или guru для визуализации вызовов функций. Это поможет выявить:
- Циклические зависимости между пакетами.
- "Мертвый" код, который нигде не вызывается.
- Критические пути, которые нельзя трогать без предварительного покрытия тестами.
Этап 2: Создание "защитной сети" из тестов
Рефакторинг без тестов — это прыжок без страховки. Ваша задача — написать характеристические тесты, которые фиксируют текущее поведение системы, даже если оно кажется неправильным.
2.1. Тестирование на уровне функций
Начните с самых критичных и изолированных частей кода. Пример для функции, которую планируется рефакторить:
// Legacy код
func CalculatePrice(base float64, discountCode string) float64 {
if discountCode == "SALE10" {
return base * 0.9
}
if discountCode == "VIP20" {
return base * 0.8
}
return base
}
// Характеристический тест
func TestCalculatePrice(t *testing.T) {
tests := []struct {
name string
base float64
code string
want float64
}{
{"без скидки", 100.0, "", 100.0},
{"скидка 10%", 100.0, "SALE10", 90.0},
{"скидка 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. Интеграционные тесты через Golden Files
Для сложных структур (JSON-ответы, генерация отчетов) используйте технику "золотых файлов". Запишите текущий вывод в файл, а при рефакторинге сравнивайте с ним.
func TestHandler(t *testing.T) {
// Выполняем запрос
req := httptest.NewRequest("GET", "/api/data", nil)
rec := httptest.NewRecorder()
myHandler(rec, req)
// Сравниваем с эталоном
expected, _ := os.ReadFile("testdata/golden.json")
if !bytes.Equal(rec.Body.Bytes(), expected) {
t.Errorf("Output mismatch")
}
}
Этап 3: Пошаговый рефакторинг с примерами
Когда тесты готовы, можно приступать к изменениям. Действуйте по принципу маленьких шагов: изменили — запустили тесты — закоммитили.
3.1. Избавление от глобальных переменных
Проблема: Глобальный логгер или конфиг, который используется во всех пакетах.
Решение: Внедрение зависимостей через структуры (Dependency Injection).
// Плохо: глобальный логгер
var log = logrus.New()
func ProcessOrder(id int) {
log.Info("Processing order: ", id)
}
// Хорошо: DI через структуру
type OrderProcessor struct {
Logger *logrus.Logger
}
func (p *OrderProcessor) Process(id int) {
p.Logger.Info("Processing order: ", id)
}
3.2. Разделение "God-функции"
Длинные функции с несколькими уровнями вложенности — главный враг читаемости. Разбивайте их на маленькие, именованные функции.
// Было: одна функция на 150 строк
func HandleHTTP(w http.ResponseWriter, r *http.Request) {
// ... 50 строк валидации
// ... 50 строк бизнес-логики
// ... 50 строк форматирования ответа
}
// Стало: композиция маленьких функций
func HandleHTTP(w http.ResponseWriter, r *http.Request) {
order, err := parseOrderRequest(r)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
result := processOrder(order)
writeJSONResponse(w, result)
}
3.3. Замена магических чисел на константы
Это улучшает читаемость и упрощает будущие изменения.
// Плохо
if status == 3 {
// ...
}
// Хорошо
const (
StatusPending = 1
StatusApproved = 2
StatusRejected = 3