Миграция с Go на Rust: честный гайд

Маттиас Эндлер, основатель Rust-консалтинга corrode и ведущий подкаста Rust in Production, выпустил большой гайд по переходу с Go на Rust. Ценен он тем, что автор не притворяется нейтральным. Он прямо пишет, что не любит Go и считает его плохо спроектированным, хоть и очень успешным языком, а его бизнес зарабатывает на Rust. При этом в тексте полно честных признаний сильных сторон Go, цитат инженеров из InfluxData, PubNub, Microsoft и Canonical и конкретных цифр по реальным миграциям. Раньше Эндлер уже сравнивал эти языки в статье «Go vs Rust? Choose Go.» 2017 года и в практическом сравнении для Shuttle. Разбираем главное.

Дело не в скорости

Go и так быстрый, и большинству бэкендов его хватает с запасом. С Go на Rust переходят не за производительностью, а за гарантиями корректности. Эндлер формулирует это так: Go оптимизирован под скорость итераций, Rust под корректность. Всё, что в Go держится на соглашениях, линтерах и проверках в рантайме, в Rust зашито в систему типов: обработка nil, проброс ошибок, гонки данных, время жизни ресурсов, отмена задач.

Хороший пример Mutex. В Rust Mutex<T> не просто сообщает, что данные нужно защищать блокировкой, он делает блокировку единственным способом до них добраться. Вызываете .lock(), получаете guard, через него работаете с данными, guard уничтожается, и блокировка снимается сама. Сценария «забыл залочить» в такой модели просто не существует.

С какими болями приходят команды

Первая и самая узнаваемая: nil-паники в продакшене. Сервис месяцами работает стабильно, пока не срабатывает ветка, где кто-то забыл проверить указатель. nilaway и staticcheck ловят часть случаев, но это опциональные инструменты, которые ничего не гарантируют. В Rust функция вернёт Option<User>, и компилятор не даст обратиться к пользователю, пока вы не обработали вариант None.

Вторая: гонки данных. go test -race отличный инструмент, но он находит только те гонки, которые реально случились во время тестов. Запись в map из двух горутин без блокировки спокойно компилируется и взрывается под нагрузкой. В Rust общий изменяемый HashMap между потоками без Arc<Mutex<…>> просто не соберётся. Пол Дикс, основатель InfluxData, называет устранение гонок главным плюсом переписывания InfluxDB на Rust: в первой версии из-за них были очень неприятные баги.

Третья: обработка ошибок. Бесконечные if err != nil размывают логику функции, оборачивание через fmt.Errorf держится на дисциплине, а компилятор не подскажет, что вы забыли обработать новый тип ошибки. В Rust варианты ошибок собираются в один enum, оператор ? пробрасывает их и сам конвертирует типы, а match проверяется на полноту. Добавили новый вариант, и компилятор покажет все места, которые нужно обновить. Чтение конфига, которое в Go занимает две проверки err с ручным оборачиванием, в Rust укладывается в три строки: read_to_string(path)?, затем serde_json::from_str(&data)? и Ok(cfg). Автор честно приводит и контраргумент гоферов: errcheck закрывает большую часть проблем, а явный if err != nil читается проще плотных цепочек с ?.

Четвёртая: хвосты задержек. GC в Go отличный, но низкие паузы не означают отсутствие пауз. При серьёзной нагрузке на память P99 начинает прыгать, а Rust-версия просто не выделяет память на горячем пути. Большинству сервисов это не мешает, но для трейдинга, сетевых прокси, RTB и высоконагруженного приёма данных это весомый аргумент. CTO PubNub Стивен Блум говорит прямо: на их масштабе нужна производительность в пересчёте на каждый доллар, и её даёт Rust.

Где Go объективно выигрывает

Самая сильная часть гайда в том, что автор не прячет минусы Rust. Главное преимущество Go, по его словам, отсутствие «раскраски функций». Любую функцию можно вызвать обычным образом или запустить через go, не меняя ни сигнатуру, ни вызывающий код. В Rust есть async fn, .await, выбор рантайма (почти всегда tokio) и ограничения Send и Sync. Именно этого бывшим гоферам не хватает больше всего.

Второй минус: время компиляции. Чистая релизная сборка среднего сервиса занимает минуты против почти мгновенной сборки в Go. Помогают cargo check в цикле разработки, разбиение на воркспейсы и вынос тяжёлых proc-macro крейтов в отдельные пакеты. У автора есть отдельная статья с советами по ускорению сборки.

Третий: экосистема в отдельных нишах. Kubernetes-операторы, SDK облачных провайдеров и драйверы редких баз данных в Go развиты лучше. Эндлер советует потратить день на аудит зависимостей до старта миграции: команды, с которыми он работает, часто дописывают одну-две библиотеки сами.

И конечно, borrow checker. Первый месяц самый тяжёлый: компилятор отказывается принимать код, который «очевидно должен работать». Совет автора в том, чтобы воспринимать ошибку не как придирку, а как вопрос к себе. Можно ли использовать значение после перемещения? Может ли один поток менять данные, пока другой их читает? Может ли ссылка пережить объект? Почти всегда проблема в коде была и раньше, просто теперь её кто-то заметил.

Острый вопрос про дженерики

Отдельный раздел посвящён дженерикам Go, которые автор называет «слишком мало и слишком поздно». Они появились в версии 1.18, через 13 лет после выхода языка, и стандартная библиотека до сих пор их почти не использует: sync.Map по-прежнему работает с any. Нет ассоциированных типов, blanket-реализаций и методов с собственными параметрами типа (предложение о них приняли в 2026 году и нацелили на Go 1.27). Реализация через GCShape stenciling ускоряет компиляцию, но добавляет косвенные вызовы, поэтому обобщённый код бывает медленнее написанного вручную, что показал известный разбор PlanetScale. В Rust дженерики мономорфизируются и лежат в основе всего языка. Впрочем, Эндлер сам признаёт, что для 95% кода эта разница не критична.

Как мигрировать и не сломать прод

Переписывать всё разом не нужно, все успешные истории переезда были методичными. Можно вынести в Rust один проблемный сервис с горячим путём, сохранив API-контракт, как в знаменитом кейсе Discord. Можно начать с воркеров, консьюмеров очередей и пайплайнов, у которых чёткие границы ввода и вывода. Можно применить паттерн strangler fig: шлюз направляет отдельные эндпоинты в новый Rust-сервис, пока тот постепенно не заменит старый. Вызов Rust из Go через cgo возможен, но для бэкенда автор его почти не рекомендует, потому что сложность сборки и накладные расходы FFI обычно не окупаются.

Практические советы тоже дельные. Начинайте с сервиса с понятными границами и небольшим радиусом поражения. Сохраняйте те же пути, JSON и формат ошибок, чтобы клиенты вообще ничего не заметили. Не пишите на Rust в стиле Go. И не пытайтесь осваивать язык между делом: это как бежать марафон без единой тренировки.

Для типичного бэкенда связка axum, sqlx, tokio, tracing, serde и clap закрывает около 90% задач. Зависимостей будет больше, чем в Go, и это реальная цена: больше изменений в Cargo.lock и шире поверхность атаки через цепочку поставок.

Что даёт переход в цифрах

По опыту миграций, которые вёл автор, потребление CPU снижается на 20-40%, памяти на 30-50%, а P99 под нагрузкой становится заметно ровнее. Но главный эффект в другом: инцидентов в проде становится намного меньше, потому что гонки данных, разыменование nil и пропущенные ошибки просто не компилируются. Дежурства после миграции, по словам Эндлера, обычно становятся очень скучными. А вот десятикратного роста пропускной способности ждать не стоит.

Итог

Хоронить Go никто не предлагает. Для Kubernetes-инструментов, CLI, тонких API-слоёв и сервисов, где скорость команды важнее строгих гарантий, он остаётся отличным выбором. Многие команды в итоге приходят к гибриду: Go для «скучных» сервисов, Rust для тех, где надёжность и производительность окупают дополнительные усилия. Rust стоит своих денег там, где цена бага в проде выше цены строгого компилятора.

Оригинал гайда: Migrating from Go to Rust на corrode.dev

Поделиться:

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *