Rust для гошника: что переносится из опыта, а где придётся переучиваться
Учу Rust в боевом режиме чуть больше года. Параллельно продолжаю писать и поддерживать сервисы на Go. Накопил список вещей, которые между этими двумя языками либо переносятся легко, либо вызывают мучительный реформат привычек.
Расскажу честно: что из Go-опыта помогает в Rust, что мешает, и что лучше отложить в сторону, чтобы не воевать с компилятором лишнюю неделю.
Что переносится из Go легко
Привычка к статической типизации без наследования
Если ты уже привык, что в Go нет классов, нет inheritance, и поведение описывается через интерфейсы — тебе будет проще, чем человеку с Java-бекграундом. В Rust есть traits, которые концептуально близки к интерфейсам Go: реализация отдельно от типа, без громоздкой иерархии.
trait Greeter {
fn greet(&self) -> String;
}
struct Russian;
impl Greeter for Russian {
fn greet(&self) -> String { "Привет".into() }
}
Это та же мысль, что Stringer в Go: контракт, который реализация берёт явно. Только в Rust ты пишешь impl Trait for Type явно, в Go — implicit.
Композиция, а не наследование
Структуры со встраиванием полей — привычная картина в Go. В Rust то же самое, только без implicit-делегирования. Если у тебя есть Service со встроенным Logger, в Go ты просто вызываешь service.Log(...). В Rust — service.logger.log(...). Чуть многословнее, но логика та же.
Композиция ошибок
Go-код регулярно делает fmt.Errorf("do X: %w", err). В Rust есть thiserror и anyhow, которые делают то же самое плюс минус: оборачивают ошибку с контекстом, сохраняют цепочку. Привычка не игнорировать err переносится один в один.
Modulesы и зависимости
Cargo концептуально — это улучшенный аналог go mod. Вместо go.sum — Cargo.lock. Локальный путь зависимости через path = "../mylib" работает как replace в go.mod. Workspaces в Rust удобнее, чем работа с многомодульным репо в Go.
Что вызывает реформат привычек
Borrow checker
Самое известное. В Go ты передаёшь объект в функцию и не думаешь — копия это, ссылка или что вообще. В Rust ты обязан явно сказать: fn(s: String), fn(s: &str), fn(s: &mut String). Каждый вариант имеет свои правила времени жизни.
Первое время бесит. Сидишь час над тем, чтобы передать строку в две функции и не наткнуться на «cannot borrow as mutable because it is also borrowed as immutable». В Go это вообще не существует как класс задач.
Что помогает — забыть про оптимизацию, начать с clone() везде. Сначала сделай работающий код, потом убирай лишние клоны. У меня в первых проектах на Rust было 100 клонов, в текущих — 3-4 на тысячу строк.
Move semantics
let s = String::from("hello");
let t = s;
println!("{}", s); // ошибка: значение перемещено в t
В Go это просто работает: оба указатели на одну строку. В Rust присваивание перемещает владение, и оригинальная переменная становится недоступна. Понять это умом легко, привыкнуть писать — сложнее.
Generics
В Go дженерики только-только появились и используются ограниченно. В Rust они везде. Vec<T>, HashMap<K, V>, Option<T>, Result<T, E> — это базовые типы, без них не написать почти ничего. Переход тяжёл тем, что нужно сразу освоить trait bounds: fn foo<T: Display + Clone>(t: T).
Lifetime аннотации
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { /* ... */ }
В Go таких аннотаций нет — GC сам решает, кто кого переживёт. В Rust иногда приходится явно писать lifetime. На начальном этапе это пугает. На самом деле в 90% случаев lifetime выводится автоматически (через elision rules), и явные 'a нужны только в нетривиальных API.
Async и его подход
В Go горутина — это всё. go func() — и работа в фоне. В Rust async — это state machine, который компилятор генерирует из async fn, и его кто-то должен poll. Этот «кто-то» — runtime: tokio, async-std, smol.
Пример того же fetch:
async fn fetch(url: &str) -> reqwest::Result<String> {
let body = reqwest::get(url).await?.text().await?;
Ok(body)
}
Каждый .await — точка, где Future может быть приостановлен. В Go ты этого не пишешь, runtime сам разруливает. В Rust ты явно расставляешь точки и платишь когнитивно за более тонкий контроль.
Что лучше отложить
Желание оптимизировать сразу
В Rust есть unsafe, raw pointers, lifetime-фокусы и SIMD. Не лезь туда первый год. 95% задач решается через Box, Arc, Rc и обычный код. Производительность всё равно будет на уровне C++ или близко.
Глубокое погружение в макросы
Декларативные и процедурные макросы Rust мощные, но писать свои в первые месяцы не стоит. Учись использовать готовые: derive, tokio::main, serde_derive, thiserror. Свой macro_rules! можно написать, когда поймёшь, зачем он именно тебе.
Все 60 features tokio сразу
tokio огромный. tokio::fs, tokio::net, tokio::time, tokio::sync, tokio::task, tokio::io. Бери ровно то, что нужно для текущей задачи, остальное смотри по мере появления.
Конкретный сценарий: переписать CLI с Go на Rust
У меня в прошлом квартале была задача — взять небольшой Go CLI и переписать на Rust для статической линковки и distribution в виде одного бинарника на musl. Что я заметил по итогам:
- Скорость разработки в Rust была примерно в 2.5 раза ниже первые две недели. Потом выровнялась с Go.
- Размер бинарника после release-сборки и strip — на 40% меньше, чем у Go (1.8 МБ против 3.1).
- Стартап CLI почти мгновенный, в Go был ощутимый startup overhead на пустом hello world.
- Объём кода вышел почти такой же, ±5%.
Это не означает, что Rust «лучше» для CLI. Это значит, что для конкретного случая (маленький бинарник, быстрый старт, статическая линковка) Rust зашёл хорошо. Для микросервиса с десятком интеграций я бы продолжал брать Go: время разработки и поддержки важнее.
Что советую читать
Книгу The Rust Programming Language — must read. Не пропускай главы про ownership и lifetimes, даже если кажется скучно. Это та самая база, без которой потом будет больно.
После книги — rustlings (упражнения) и пара пет-проектов. Хорошо берётся то, что ты уже делал в Go: parser, маленький HTTP-сервер, CLI-утилита. Сравнение «как это было на Go» очень помогает не перебарщивать с абстракциями.
Через полгода втянешься. Через год начнёшь видеть, где Rust даёт реальный выигрыш и где он избыточен. Без воинствующей позиции «Rust лучше всего» — это просто другой инструмент с другим балансом плюсов и минусов.