Представьте: сервис на Go обрабатывает 10 000 запросов в секунду, но при пиковой нагрузке неожиданно падает с out of memory. Разработчик добавил горутину для каждого запроса — красиво, но без ограничения параллелизма. Знакомо? Это классическая ситуация, с которой начинается путь в современное системное программирование. Go и Rust часто противопоставляют, но на практике они решают разные задачи. Разберём, как это работает, где люди ошибаются и что важно знать до того, как выбирать инструмент.
Почему Go и Rust, а не что-то одно
Go создавался для простоты и скорости разработки. Его среда выполнения (runtime) берёт на себя управление горутинами, сборку мусора и сетевой ввод-вывод. Вы пишете прямолинейный код, а планировщик раскладывает горутины по потокам ОС. Это удобно для микросервисов, API и бэкенда, где важна пропускная способность и низкая задержка при большом числе одновременных соединений.
Rust решает другую проблему — безопасность работы с памятью без сборщика мусора. Система владения и заимствования проверяется на этапе компиляции, поэтому в рантайме нет ни GC-пауз, ни гонок данных (data races). Это критично для утилит, драйверов, WebAssembly-модулей и всего, где предсказуемость важнее скорости написания кода.
Ключевое различие: Go оптимизирует время разработчика, Rust — время выполнения и надёжность. Выбор между ними — не религия, а инженерный компромисс.
Разбор задачи: кэширующий прокси на Go
Возьмём типичную задачу — HTTP-прокси, который кэширует ответы внешнего API. На Go это делается в несколько десятков строк: http.Server, sync.Map для кэша, горутины для обработки запросов. Но дьявол в деталях.
Шаг 1. Ограничение параллелизма. Если на каждый запрос запускать горутину без ограничения, при 10 000 RPS и времени ответа бэкенда 100 мс число одновременных горутин достигнет 1000. Каждая занимает стек от 2 КБ (начальный размер, может расти). Итого 2 МБ — немного. Но если обработчик делает запрос к БД с таймаутом 5 секунд, число горутин вырастет до 50 000, а память — до 100 МБ и больше. Лечится пулом воркеров или семафором на канале.
Шаг 2. Кэш с TTL. sync.Map не умеет автоматически удалять устаревшие записи. Нужен фоновый процесс, который проходит по ключам и чистит их. Если этого не сделать, память течёт. В Rust для такой задачи взяли бы DashMap с ручным управлением временем жизни или крейт moka с встроенным TTL — но там пришлось бы разбираться с владением и временами жизни.
Шаг 3. Профилирование. В Go встроен pprof: достаточно импортировать net/http/pprof и снять профиль по адресу /debug/pprof/. Видно, сколько времени тратится на сборку мусора, какие функции греют процессор. В Rust аналоги — perf и flamegraph, но требуют внешних инструментов.
Что пойдёт не так, если пропустить шаг 1? Приложение съест память и упадёт по OOM. Если пропустить шаг 2 — то же самое, но медленнее. Если не профилировать — будете гадать, почему latency растёт.
Где ломается сама схема
Даже правильный выбор языка не гарантирует успеха. Вот четыре сценария, где отказывает сама модель.
1. Изменились условия нагрузки
Сервис на Go с горутинами на запрос работал при 1000 RPS. После маркетинговой кампании нагрузка выросла до 20 000 RPS. Планировщик Go не справляется с очередью горутин, задержки растут лавинообразно. Как заметить заранее: метрика go_goroutines и go_sched_latencies_seconds. Что делать: ввести ограничение параллелизма и backpressure (например, через golang.org/x/sync/semaphore).
2. Правило не применяется
В Rust borrow checker запрещает изменять данные, пока на них есть ссылка. В многопоточном коде это выливается в необходимость Arc<Mutex<T>>. Если попытаться обойтись без блокировок через unsafe, можно получить гонку данных, которую компилятор не поймает. Как заметить: тесты под нагрузкой с -race в Go или loom в Rust. Что делать: не злоупотреблять unsafe без ревью.
3. Инструмент перестал работать
Профилировщик pprof показывает утечку горутин, но не говорит, где именно они создаются. Приходится вручную искать go func(). В Rust flamegraph может не показать inline-функции. Что делать: добавлять контекст в логи и использовать трассировку (OpenTelemetry).
4. Срок истёк
Выбрали Rust для нового микросервиса, но команда не знает языка. Время до продакшена растягивается втрое. Как заметить заранее: оцените время на изучение владения и заимствования — обычно 2–3 месяца до уверенного кода. Что делать: начать с Go для бизнес-логики, а Rust внедрять точечно для критичных по производительности модулей.
Пути освоения и их цена
Самостоятельное изучение по документации и статьям — бесплатно, но фрагментарно. Ошибки в конкурентности на Go или во владении в Rust дорого стоят: баги проявляются под нагрузкой. Курсы дают структуру и обратную связь, но стоят денег. Практика в опенсорсе — лучший тренажёр, но требует времени на поиск задач.
В курсе Go и Rust — системное программирование разбирают именно такие сценарии: ограничение параллелизма, профилирование, работа с памятью. Уроки персонализированы под уровень студента, а развёрнутые ответы можно обсудить в AI-чате.
Если хотите глубже понять, как обеспечивать надёжность сервисов, посмотрите материал Site Reliability Engineering (SRE): SLI/SLO, бюджеты ошибок и рутина.
Кому подходит, а кому нет
Подходит, если вы пишете бэкенд на Go и хотите разобраться в конкурентности и профилировании, или если вам нужно освоить Rust для безопасной работы с памятью в утилитах и WebAssembly. Не подходит, если вы ждёте быстрого результата без изучения теории: Rust требует времени на освоение владения, а Go — на понимание планировщика.
Курс Go и Rust — системное программирование даёт структурированный путь и разбор реальных задач, которые сложно найти в документации. Начать можно на странице курса.