Миграция с GO на RUST что стоит учитывать
Представь: твой Go-сервис работает месяцами без проблем, а потом в 3 часа ночи прилетает алерт — segmentation fault. Кто-то не проверил указатель на nil, и вся горутина падает.
Знакомая ситуация? Мне — да. И это одна из причин, почему команды начинают смотреть в сторону Rust.
Наткнулся на гайд по миграции с Go на Rust от Matthias Endler — консультанта, который помогает командам с такими переходами. Не буду пересказывать документ дословно, а разберу ключевые идеи, которые реально стоит учитывать.
Зачем вообще думать о миграции
Главное заблуждение: разработчики едут в Rust не потому, что Go медленный. Для большинства бэкенд-задач Go достаточно быстр.
Проблемы в другом:
- nil-переполнение в продакшене. Кто-то забыл проверить указатель, и горутина падает. Классика. Линтеры типа nilaway ловят часть таких случаев, но они опциональны и не работают через границы пакетов.
- Гонки данных, которые не поймал -race. Флаг
go test -race— отличный инструмент. Но он работает только во время тестов и только на том коде, который реально выполняется. В продакшене под нагрузкой может выстрелить совсем другой путь. - Слабая система типов. Go долго жила без generics, а интерфейсы — не полноценная замена трейтам. Тип Set? В стандартной библиотеке его нет. Приходится городить
map[T]struct{}.
Автор гайда формулирует это так: в Go многое держится на конвенциях, инструментах и рантайм-детекте. В Rust это кодируется прямо в систему типов, которую проверяет компилятор.
Что даёт система типов RUST на практике
Здесь начинается самое интересное.
В Rust Option<T> — это тип, который заставляет тебя обработать случай None. Ты физически не можешь разыменовать Option без проверки. Компилятор не пропустит.
fn handle(&self, req: &Request) -> Result<(), ServiceError> {
let user = self.repo.find(req.user_id)?; // ? означает "верни ошибку если None"
user.notify()
}
В Go тот же код выглядит примерно так:
user, err := s.repo.Find(req.UserID)
if err != nil {
return err
}
return user.Account.Notify() // а если Account == nil?
Разница в том, что в Go компилятор не заставляет тебя думать о None-case. В Rust — заставляет.
С гонками данных аналогично. В Rust shared mutable state между потоками можно только через типы, которые реализуют Send и Sync. Попробуй передать HashMap между потоками — программа просто не скомпилируется.
Сравнение подходов к безопасности
─────────────────────────────────
Go: Конвенции → Инструменты → Рантайм
▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░
(некоторые ошибки ловятся,
многие — только в проде)
Rust: Типы → Компилятор → Готово
▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓
(большинство ошибок
невозможно написать)
Это не магия. Это архитектура языка. Мутабельный доступ к данным в Rust требует либо эксклюзивного владения (данные у тебя одного), либо обёртки типа Arc<Mutex<...>>. Тип делает контракт явным.
Инструментарий: где RUST и GO похожи
Хорошая новость: если ты привык к Go toolchain, Rust не станет шоком.
Cargo — это как go build + go test + go doc + go get в одном флаконе. Базовые вещи встроены, расширения ставятся одной командой и чувствуются нативно.
Экосистема инструментов ─────────────────────── ┌─────────────┬────────────────────┐ │ Задача │ Rust │ ├─────────────┼────────────────────┤ │ Сборка │ cargo build │ │ Тесты │ cargo test │ │ Форматир. │ cargo fmt │ │ Линтинг │ cargo clippy │ │ Документация│ cargo doc --open │ │ Профилировани│ cargo flamegraph │ │ Зависимости │ cargo add/update │ └─────────────┴────────────────────┘ (Всё из коробки, расширения — через cargo install)
Оба языка сошлись на важной идее: единый каноничный стиль важнее идеального стиля. gofmt и rustfmt не нравятся всем одинаково — но они устраняют споры о форматировании в код-ревью. Это стоит больше, чем любые личные предпочтения.
Как выбирать, что мигрировать первым
Автор гайда честно говорит: не начинай с критичного ядра. Это не та тема, где стоит экспериментировать.
Критерии первого модуля:
- Небольшой объём. Максимум несколько тысяч строк.
- Чёткие границы. Минимум зависимостей от остальной системы.
- Хорошее покрытие тестами. Ты должен быть уверен, что поведение не меняется.
- Низкий риск для бизнеса. Если упадёт — никто не умрёт.
Идеальный кандидат — какой-нибудь утилитарный сервис: парсер, конвертер, воркер для фоновых задач.
Пошаговый план миграции
Вот подход, который имеет смысл:
- Аудит текущего кода. Пойми, что у тебя есть. Какие зависимости, какие внешние сервисы, какие контракты.
- Выбери пилотный модуль. По критериям выше.
- Параллельная реализация. Не удаляй Go-код, а пиши Rust-версию рядом.
- Тесты на паритет поведения. Запускай оба, сравнивай выходные данные.
- Поэтапное переключение трафика. Сначала 1%, потом 10%, потом 100%.
- Удаление старого кода. Только после уверенности в новом.
На каждом шаге нужны метрики. Время ответа, количество ошибок, использование памяти. Без данных ты не поймёшь, стало лучше или хуже.
Типичные ошибки
- Хотеть мигрировать всё и сразу. Это путь в ад. Делай итеративно.
- Не писать тесты перед миграцией. Если покрытия нет — добавь сначала.
- Забыть про rollback-план. Что делать, если новая версия падает? Как быстро вернуться на старую?
- Игнорировать разницу в философии. Rust требует больше усилий на старте, но меньше — на поддержку. Если команда этого не понимает, будут конфликты.
Стоит ли оно того?
Честный ответ: зависит от команды.
Если у тебя постоянные инциденты из-за nil-pointer errors и гонок данных — да. Если Go-сервисы работают стабильно и проблем не видно — вероятно, нет.
Rust сложнее учится. Компиляция медленнее, чем у Go. Но часть ошибок, которые раньше ловил прод, здесь ловит компилятор. Это инвестиция в надёжность.
Мне нравится мысль из гайда: в Rust проверки уходят в систему типов, и язык становится «сложнее на старте, но легче держать правильным». once you internalize that pattern, Rust stops feeling heavy and starts feeling like the compiler is doing work you used to do in your head.
Главное — не романтизировать. Оба языка работают. Выбирай тот, который решает твою проблему.
Выводы
- Rust имеет смысл там, где цена ошибки в продакшене высока.
- Начинать миграцию стоит с маленького, изолированного и хорошо протестированного модуля.
- Главная ценность Rust — перенос части проверок из рантайма в компилятор.
- Миграция должна быть поэтапной, с метриками и планом отката.
Ссылки
- Migrating from Go to Rust — Matthias Endler — гайд по переходу с Go на Rust
- Just Fucking Use Go — Blain Smith — альтернативная точка зрения для объективности
- Rust vs Go: A Hands-On Comparison — Shuttle — практическое сравнение Rust и Go
Дмитрий Полухин — продуктовый дизайнер. Пишу про разработку, AI и дизайн интерфейсов. Обо мне, контакты и профили.