lenec ru

← все посты

Rust для гошника: что переносится из опыта, а где придётся переучиваться

11K

Учу 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.sumCargo.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 лучше всего» — это просто другой инструмент с другим балансом плюсов и минусов.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.