Рефакторинг legacy кода на Go: от хаоса к порядку

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему 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                

Блоги

Рекомендаций по книгам