Рефакторинг legacy кода на Go: полное руководство от А до Я

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

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

Начать курс

Введение: почему legacy-код на Go требует рефакторинга?

Go (Golang) славится своей простотой, производительностью и встроенными инструментами для параллелизма. Однако любой проект со временем обрастает "техническим долгом": глобальные переменные, отсутствие интерфейсов, магические числа, нечитаемые функции и отсутствие тестов. Такой код называют legacy. Рефакторинг — это не переписывание всего с нуля, а постепенное улучшение структуры без изменения внешнего поведения. В этой статье мы разберем пошаговый план рефакторинга legacy кода на Go, от анализа до внедрения.

Шаг 1: Диагностика legacy-кода

Прежде чем что-то менять, нужно понять, с чем мы имеем дело. Основные "красные флаги" в Go-проектах:

  • Глобальные переменные (var db *sql.DB) — усложняют тестирование и параллельную работу.
  • Функции-монстры (более 50-100 строк) — делают код нечитаемым.
  • Отсутствие интерфейсов — жесткая связь с конкретными реализациями.
  • Магические строки/числа — затрудняют поиск и изменение.
  • Отсутствие тестов — рефакторинг без тестов — это хождение по тонкому льду.

Инструменты для анализа: используйте go vet и staticcheck для поиска подозрительных конструкций. Также полезен gocyclo для вычисления цикломатической сложности.

Пример проблемного кода:

package main

import "fmt"

var config map[string]string // глобальная переменная

func main() {
    config = map[string]string{"port": "8080"}
    startServer()
}

func startServer() {
    // магическая строка "port"
    fmt.Println("Starting on port:", config["port"])
    // ... 100 строк кода
}

Шаг 2: Создание защитной сети из тестов

Без тестов рефакторинг превращается в гадание. Ваша задача — покрыть ключевую логику тестами до изменений. Используйте подход Golden Files (эталонные файлы) для сложных выводов или интеграционных тестов.

Пример добавления теста для функции из legacy-кода:

// legacy.go (оригинал)
func CalculateDiscount(price float64, isPremium bool) float64 {
    if isPremium {
        return price * 0.9
    }
    return price * 0.95
}

// legacy_test.go
package main

import "testing"

func TestCalculateDiscount_Premium(t *testing.T) {
    result := CalculateDiscount(100.0, true)
    expected := 90.0
    if result != expected {
        t.Errorf("Expected %f, got %f", expected, result)
    }
}

func TestCalculateDiscount_Regular(t *testing.T) {
    result := CalculateDiscount(100.0, false)
    expected := 95.0
    if result != expected {
        t.Errorf("Expected %f, got %f", expected, result)
    }
}

Совет: используйте go test -cover для оценки покрытия. Старайтесь покрыть не менее 70% бизнес-логики перед началом рефакторинга.

Шаг 3: Постепенная декомпозиция и выделение интерфейсов

Основная цель рефакторинга — сделать код тестируемым и гибким. Начните с выделения интерфейсов для внешних зависимостей (базы данных, HTTP-клиенты, файловые системы).

3.1 Выделение интерфейса вместо прямого использования БД

Допустим, у нас есть функция, которая напрямую работает с *sql.DB:

// legacy code
func GetUser(db *sql.DB, id int) (*User, error) {
    row := db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)
    // ...
}

Рефакторинг: создаем интерфейс UserRepository:

type UserRepository interface {
    GetByID(id int) (*User, error)
}

type userRepo struct {
    db *sql.DB
}

func (r *userRepo) GetByID(id int) (*User, error) {
    row := r.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id)
    // ...
    return &User{}, nil
}

// Теперь бизнес-логика зависит от интерфейса, а не от конкретной БД
func GetUserService(repo UserRepository, id int) (*User, error) {
    return repo.GetByID(id)
}

Это позволяет легко подменить реализацию в тестах (например, на in-memory хранилище).

3.2 Избавление от глобальных переменных

Глобальные переменные — главный враг тестирования. Замените их на внедрение зависимостей (Dependency Injection):

// Вместо глобальной переменной
var logger *log.Logger

// Создаем структуру, которая принимает логгер
type Service struct {
    logger *log.Logger
}

func NewService(logger *log.Logger) *Service {
    return &Service{logger: logger}
}

func (s *Service) DoSomething() {
    s.logger.Println("Doing something")
}

Шаг 4: Рефакторинг больших функций и устранение дублирования

Функции длиной более 30-40 строк — кандидаты на разбиение. Используйте принцип единой ответственности (Single Responsibility).

Пример: функция, которая парсит данные и отправляет их в БД:

// До рефакторинга
func ProcessData(data string) error {
    // 10 строк парсинга
    // 15 строк валидации
    // 20 строк сохранения в БД
    return nil
}

// После рефакторинга
func ParseData(data string) (ParsedData, error) {
    // только парсинг
}

func ValidateData(d ParsedData) error {
    // только валидация
}

func SaveData(d ParsedData) error {
    // только сохранение
}

func ProcessData(data string) error {
    d, err := ParseData(data)
    if err != nil {
        return err
    }
    if err := ValidateData(d); err != nil {
        return err
    }
    return SaveData(d)
}

Такой код легче тестировать, отлаживать и изменять.

Заключение

Рефакторинг legacy кода на Go — это не страшная задача, если подходить к ней системно. Помните три главных правила:

  1. Не ломайте

Блоги

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