Рефакторинг Go кода: когда и как не сломать проект
Вы когда-нибудь открывали свой старый Go-код и чувствовали лёгкую тоску? Переменные по имени a и b, функции длиной в экран, а главный цикл обработки запросов напоминает лабиринт Минотавра. Знакомо? Добро пожаловать в мир, где рефакторинг — не роскошь, а необходимость.
Go славится своей простотой и скоростью компиляции, но это не значит, что код пишется сам собой идеальным. Рано или поздно каждый разработчик сталкивается с вопросом: «Когда же пора наводить порядок?». В этой статье мы разберём, как делать рефакторинг Go кода без страха и упрёка, с реальными примерами и советами от практиков.
Когда рефакторинг Go кода необходим: красные флаги
Рефакторинг — это не про «переписать всё с нуля». Это про улучшение внутренней структуры без изменения внешнего поведения. Вот явные сигналы, что пора браться за инструменты:
- Код пахнет (code smell): дублирование, длинные функции, магические числа.
- Трудно добавлять новые фичи: каждый патч ломает старые тесты.
- Медленные тесты: если юнит-тесты идут дольше 10 секунд, структура хромает.
- Много if/else или switch: особенно когда появляются новые типы.
- Дикая вложенность:
if err != nil { if ... }— ад отладки.
В Go особенно важно следить за обработкой ошибок и интерфейсами. Если вы ловите себя на том, что пишете interface{} там, где нужен конкретный тип, — это звоночек.
Этапы рефакторинга: от плана до коммита
Рефакторинг без стратегии — путь к хаосу. Придерживайтесь простого алгоритма:
- Поймите, что именно не так. Напишите список проблем: дублирование, сложность, медленные участки.
- Напишите тесты. Без тестов рефакторинг — русская рулетка. Используйте
go testи покрывайте критичные пути. - Делайте маленькие шаги. Один логический блок за раз. Коммит после каждого успешного изменения.
- Запустите тесты и бенчмарки. Убедитесь, что производительность не упала.
- Проверьте код-ревью. Свежий взгляд коллеги найдет то, что вы упустили.
Практические примеры рефакторинга Go кода
Теория — хорошо, но давайте посмотрим на живой код. Ниже — три типичные ситуации и их решения.
1. Избавляемся от дублирования с помощью функций
Допустим, у вас есть код, который парсит конфигурацию и проверяет ошибки одинаковым образом:
// Плохо: дублирование логики проверки
func loadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read config: %w", err)
}
var cfg Config
err = json.Unmarshal(data, &cfg)
if err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
return &cfg, nil
}
func loadSecrets(path string) (*Secrets, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read secrets: %w", err)
}
var sec Secrets
err = json.Unmarshal(data, &sec)
if err != nil {
return nil, fmt.Errorf("parse secrets: %w", err)
}
return &sec, nil
}
Решение: выносим общую логику в утилитарную функцию.
// Хорошо: единый метод для чтения и парсинга JSON
func readJSON[T any](path string) (*T, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read file %s: %w", path, err)
}
var result T
err = json.Unmarshal(data, &result)
if err != nil {
return nil, fmt.Errorf("unmarshal %s: %w", path, err)
}
return &result, nil
}
// Теперь вызываем дженерик-функцию
func loadConfig(path string) (*Config, error) {
return readJSON[Config](path)
}
func loadSecrets(path string) (*Secrets, error) {
return readJSON[Secrets](path)
}
2. Упрощаем вложенные if-ы с помощью guard clauses
Глубокие вложенности — бич Go, особенно при обработке ошибок.
// Плохо: глубокое вложение
func processOrder(order *Order) error {
if order != nil {
if order.Status == "pending" {
if err := validatePayment(order); err != nil {
return err
}
// ... ещё 10 строк кода
} else {
return errors.New("order not pending")
}
} else {
return errors.New("order is nil")
}
return nil
}
Решение: используем ранний возврат (guard clauses).
// Хорошо: плоская структура
func processOrder(order *Order) error {
if order == nil {
return errors.New("order is nil")
}
if order.Status != "pending" {
return errors.New("order not pending")
}
if err := validatePayment(order); err != nil {
return fmt.Errorf("payment validation failed: %w", err)
}
// основная логика здесь
return nil
}
3. Замена магических строк на константы и типы
Магические строки — враг поддерживаемости. Одна опечатка — и баг живёт годами.
// Плохо: строки разбросаны по коду
func getDiscount(userType string) float64 {
switch userType {
case "vip":
return 0.2
case "regular":
return 0.1
default:
return 0.0
}
}
Решение: вводим тип-перечисление и константы.
// Хорошо: типизированные константы
type UserType string
const (
UserVIP UserType = "vip"
UserRegular UserType = "regular"
)
func getDiscount(userType UserType) float64 {
switch userType {
case UserVIP:
return 0.2
case UserRegular:
return 0.1
default:
return 0.0
}
}
Инструменты для рефакторинга Go кода
Go-экосистема богата инструментами, которые облегчают жизнь. Вот мой топ:
- go fmt / gofumpt — автоматическое форматирование. Спор о пробелах закончен.
- golangci-lint — сборник линтеров. Найдет мёртвый код, неправильные имена и потенциальные