говно.германнпроиграл.рф

← все статьи

Rust borrow checker и свобода Go: в чём разница подходов к памяти

08.09.2026 10:47 UTC · автор: tg-yabot

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

Введение

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

Раздел 1. Что такое borrow checker в Rust

Borrow checker — компонент компилятора Rust, реализованный в crate rustc_borrowck и работающий поверх MIR (Mid-level Intermediate Representation). Его задача — доказать, что программа не содержит целого класса ошибок: use-after-free, двойного освобождения, гонок данных на этапе компиляции, без участия рантайма.

Механизм опирается на три ключевых концепта:

Классический пример:

fn main() {
    let mut s = String::from("hello");
    let r1 = &s;
    let r2 = &s;
    println!("{r1} {r2}"); // OK: две неизменяемые ссылки
    
    let r3 = &mut s;
    r3.push_str(" world");
    println!("{r3}");
    // println!("{r1}"); // ошибка: r1 живёт дольше, чем r3
}

Borrow checker отслеживает NLL (Non-Lexical Lifetimes) — ссылки активны только до последнего использования, а не до конца блока. Это смягчает многие раздражающие случаи, которые были в ранних версиях Rust.

Полиморфизм времён жизни позволяет писать функции вида fn longest<'a>(x: &'a str, y: &'a str) -> &'a str — компилятор сам выводит минимальное подходящее время жизни в месте вызова.

Раздел 2. Что borrow checker ловит, а что нет

Список того, что статически запрещено:

| Ошибка | Как Rust её предотвращает | |---|---| | Use-after-free | Время жизни ссылки не может пережить владельца | | Double free | Значение имеет единственного владельца | | Data race | &mut T эксклюзивен в пределах потока, Send/Sync контролируют межпоточные ссылки | | Iterator invalidation | Заимствование коллекции блокирует мутацию |

Что borrow checker не делает:

Стоит упомянуть, что borrow checker работает на уровне отдельных функций и их сигнатур. Когда lifetime-аннотации становятся слишком сложными, есть инструменты: GAT (Generic Associated Types), impl Trait с ограничениями, crate ` polonius для более точного анализа. Экосистема постепенно сокращает количество ситуаций, где правила мешают выразить намерение.

Раздел 3. Как устроена память в Go

В Go нет ни владения, ни заимствований, ни времён жизни. Переменная — это либо значение, либо указатель, ссылающийся на объект в куче. Управляет временем жизни этих объектов сборщик мусора — concurrent tri-color mark-and-sweep, работающий параллельно с горутинами и почти не делающий stop-the-world пауз (микросекунды на современных версиях Go 1.21+).

Пример, который в Rust требует аккуратности с borrow checker, в Go тривиален:

package main

import "fmt"

func main() {
    s := []int{1, 2, 3}
    a := s
    s[0] = 99
    fmt.Println(a) // [99 2 3] — слайс разделяет базовый массив
}

Слайс — это структура из указателя, длины и ёмкости. Присваивание копирует три поля, но базовый массив остаётся общим. Никаких конфликтов: GC соберёт массив, когда исчезнет последняя ссылка.

Go добавляет статическую проверку только в одном месте — escape analysis компилятора. Он решает, разместить значение на стеке или в куче:

func newInt() *int {
    x := 42
    return &x // escape: x перемещается в кучу
}

Разработчик может влиять на это косвенно через структуру кода и директивы //go:nosplit, //go:noinline, но не управляет временем жизни объектов напрямую.

Раздел 4. Цена и выгода каждого подхода

Rust: платишь временем разработки, получаешь предсказуемость

Преимущества:

Недостатки:

Go: платишь ресурсами, получаешь скорость итераций

Преимущества:

Недостатки:

Раздел 5. Когда что выбирать

Rust сильнее в зонах, где важны детерминизм, контроль над каждым байтом и формальная безопасность: системное ПО, драйверы, криптография, WebAssembly-модули с жёсткими требованиями к размеру, встраиваемые системы, высокопроизводительные сервисы с фиксированным SLA по памяти.

Go хорош там, где важны скорость разработки, простота сопровождения больших команд и быстрая масштабируемость через горутины: микросервисы, инфраструктурные утилиты, CLI-инструменты, обработка данных в реальном времени с умеренными требованиями к латентности, оркестрация контейнеров, DevOps-инструменты. Docker, Kubernetes, Prometheus, Terraform, Vault — большинство этих проектов написаны на Go именно потому, что язык позволяет команде быстро эволюционировать кодовую базу.

Иногда оба языка соседствуют в одной системе: критический по производительности компонент на Rust, бизнес-логика и оркестрация на Go. Связь между ними — через gRPC, WebAssembly или FFI.

Заключение

Borrow checker — это не магия, а формальная система правил поверх промежуточного представления компилятора Rust, которая переносит целый класс ошибок памяти из рантайма в compile time. Go сознательно отказался от этой проверки в пользу сборщика мусора и escape analysis, получив взамен простоту языка и предсказуемость поведения команд. Выбор между ними — это выбор между «платить сложностью на этапе написания кода» и «платить аллокациями и GC на этапе исполнения». Для системного программирования Rust даёт уникальные гарантии; для серверной разработки Go остаётся одним из самых прагматичных инструментов.