Почему Go, Rust и Zig такие разные: ценности, компромиссы и назначение языков
- вторник, 16 декабря 2025 г. в 00:00:08
Команда Go for Devs подготовила перевод статьи о трёх языках, которые всё чаще оказываются в одном ряду, но на самом деле решают принципиально разные задачи: Go, Rust и Zig. Автор несколько месяцев изучал каждый из них и попытался понять, какие ценности стоят за их устройством. Go выбирает минимализм и корпоративную предсказуемость, Rust — безопасность и максимальную выразительность, Zig — радикальный контроль и отказ от ООП-мышления. Получился честный и местами провокационный разбор того, зачем нужны эти языки и кому из разработчиков они подходят.
Недавно я понял, что вместо того, чтобы выбирать «правильный инструмент для задачи», я просто пользуюсь инструментом, который есть на работе, и именно этим в основном определяется набор языков, которые я знаю. Поэтому последние пару месяцев я потратил много времени на эксперименты с языками, с которыми не сталкиваюсь в профессиональной среде. Моя цель была не в том, чтобы набраться опыта; мне интереснее выработать мнение о том, для чего каждый язык подходит лучше всего.
Языки программирования различаются по такому количеству параметров, что сравнивать их бывает трудно: легко скатиться к очевидному, но 1) совершенно скучному и 2) мало полезному выводу, что у всего есть свои компромиссы. Конечно, есть. Важный вопрос в другом: почему этот язык выбрал именно такой набор компромиссов?
Этот вопрос интересен мне потому, что я не хочу выбирать язык по списку возможностей так, будто покупаю увлажнитель воздуха. Я занимаюсь разработкой и мне важны мои инструменты. Языки, принимая свои компромиссы, выражают определенный набор ценностей. Я хочу понять, какие из них мне близки.
Этот вопрос также помогает прояснить различия между языками, которые по факту имеют сильно пересекающиеся наборы возможностей. Если судить по количеству онлайн-вопросов вроде «Go vs. Rust» или «Rust vs. Zig», люди реально путаются. Трудно удержать в голове, что язык X лучше подходит для веб-сервисов, потому что в нем есть возможности a, b и c, а язык Y располагает только a и b. А вот запомнить, что язык X лучше для веб-сервисов, потому что язык Y создан человеком, который ненавидит интернет (представим себе) и считает, что его надо отключить, куда проще.
Здесь я собрал свои впечатления о трех языках, с которыми работал в последнее время: Go, Rust и Zig. Я попытался свести собственный опыт к общему выводу о том, что ценит каждый язык и насколько хорошо он эти ценности реализует. Это может звучать упрощенно, но, честно говоря, создание набора упрощенных предубеждений как раз и есть моя цель.
Go выделяется своим минимализмом. Его описывают как «современный C». Go, конечно, не похож на C в том смысле, что у него есть сборщик мусора и полноценный рантайм, но он похож на C тем, что весь язык умещается у вас в голове.
И уместить его в голове можно потому, что в Go очень мало возможностей. Долгое время Go был печально известен отсутствием дженериков. Это наконец исправили в Go 1.18, но только после 12 лет просьб добавить дженерики в язык. Другие типичные для современных языков возможности, вроде тегированных объединений или синтаксического сахара для обработки ошибок, по-прежнему в Go не представлены.
Похоже, команда разработки Go поднимает планку для включения новых возможностей очень высоко. Итогом становится язык, который заставляет писать много шаблонного кода там, где в другом языке можно выразить то же самое куда лаконичнее. Но в результате язык получается стабильным и хорошо читаемым.
Еще один пример минимализма Go — тип slice. В Rust и Zig тоже есть slice, но там это исключительно толстые указатели. В Go slice — это толстый указатель на непрерывный участок памяти, но он еще и может расти, то есть берет на себя функции Rust’овского Vec<T> и Zig’овского ArrayList. Плюс, поскольку память управляется Go, он сам решает, хранить ли базовый массив slice на стеке или в куче; в Rust или Zig разработчику приходится гораздо серьезнее думать о размещении данных.
Миф о происхождении Go, насколько я понимаю, примерно таков: Роб Пайк устал ждать, пока проекты на C++ скомпилируются, и устал от того, что другие разработчики в Google совершают ошибки в тех же C++ проектах. Поэтому Go прост там, где C++ вычурен. Это язык для рядовых программистов, рассчитанный на то, чтобы покрывать 90% задач и быть при этом понятным, в том числе (и, возможно, особенно) при написании конкурентного кода.
Я не использую Go на работе, но, кажется, должен бы. Минимализм Go направлен на корпоративную совместную разработку. И это не упрек: создание ПО в корпоративной среде имеет свои сложности, и Go неплохо их решает.
Если Go стремится к минимализму, то Rust идет в противоположную сторону и выбирает максимализм. С Rust часто связывают слоган «абстракции без накладных расходов». Я бы уточнил его так: «абстракции без накладных расходов, и их много!»
Rust заслужил репутацию языка, который трудно выучить. Я согласен с Джейми Брендоном, который пишет, что дело не во временах жизни, а в количестве концепций, напиханных в язык. Я не первый, кто обращает внимание на этот комментарий на Github, но он идеально демонстрирует концептуальную плотность Rust:
Тип
Pin<&LocalType>реализуетDeref<Target = LocalType>, но не реализуетDerefMut. ТипыPinи&помечены как#[fundamental], поэтому реализацияDerefMutдляPin<&LocalType>в принципе возможна. В качествеLocalTypeможно использоватьSomeLocalStructилиdyn LocalTrait, и вы можете привестиPin<Pin<&SomeLocalStruct>>кPin<Pin<&dyn LocalTrait>>(да, два слояPin!). Это позволяет создать пару «умных указателей, которые реализуютCoerceUnsized, но ведут себя странно» в стабильной версии Rust (Pin<&SomeLocalStruct>иPin<&dyn LocalTrait>становятся умными указателями с «необычным поведением» и уже реализуютCoerceUnsized).
Rust, конечно, не стремится к максимализму так же намеренно, как Go стремится к минимализму. Rust сложен потому, что он пытается выполнить два требования одновременно — безопасность и производительность — которые частично противоречат друг другу.
Цель производительности понятна сама по себе. С «безопасностью» все менее очевидно; по крайней мере, для меня, возможно потому, что я слишком долго жил в питонячем мире. «Безопасность» означает «безопасность работы с памятью»: вы не должны иметь возможности разыменовать некорректный указатель, сделать double-free и так далее. Но это не всё. «Безопасная» программа избегает любого неопределенного поведения (UB).
Что такое пугающий UB? Лучше всего понять его, вспомнив, что для работающей программы есть вещи ХУЖЕ СМЕРТИ. Если в программе что-то пошло не так, немедленное завершение — это великолепный исход! Потому что альтернатива, если ошибка не перехвачена, — программа уходит в сумеречную зону непредсказуемости, где её поведение определяется тем, какой поток победит в гонке данных, или каким мусором окажется забит конкретный адрес памяти. Так появляются «гейзенбаги» и уязвимости. Очень плохо.
Rust стремится предотвратить UB без каких-либо накладных расходов в рантайме, проверяя потенциальные проблемы на этапе компиляции. Компилятор Rust умен, но не всевидящ. Чтобы он мог проверить ваш код, ему нужно понять, что именно будет происходить во время выполнения. Поэтому Rust располагает выразительной системой типов и целым зоопарком трейтов, которые позволяют вам объяснить компилятору то, что в другом языке просто являлось бы очевидным поведением рантайма.
Это делает Rust сложным, потому что вы не можете просто «взять и сделать». Прежде вы должны узнать, как Rust называет эту вещь, найти нужный трейт или механизм и реализовать его так, как ожидает Rust. Но если вы это делаете, Rust может дать гарантии о поведении вашего кода, которые недоступны другим языкам. И что не менее важно, Rust может дать такие же гарантии о чужом коде, что делает использование библиотек очень удобным. Это же объясняет, почему проекты на Rust имеют почти столько же зависимостей, сколько и проекты в экосистеме JavaScript.
Из этих трех языков Zig самый новый и наименее зрелый. На момент написания он всего лишь версии 0.14. В стандартной библиотеке почти нет документации, и лучший способ разобраться в ней — изучать исходники напрямую.
Не знаю, насколько это соответствует действительности, но мне нравится думать о Zig как о реакции и на Go, и на Rust. Go прост, потому что скрывает реальные детали работы компьютера. Rust безопасен, потому что заставляет вас пролезать через кучу своих обручей. Zig хочет вас освободить! В Zig вы управляете всей вселенной, и никто не может вам указывать.
В Go и Rust выделить объект в куче проще простого: достаточно вернуть из функции указатель на структуру. Выделение происходит неявно. В Zig вы выделяете каждый байт самостоятельно и явно. (В Zig ручное управление памятью.) Контроля здесь даже больше, чем в C: чтобы выделить память, нужно вызвать alloc() на конкретном аллокаторе. То есть вы должны заранее выбрать реализацию аллокатора, которая лучше всего подходит вашему случаю.
В Rust создать изменяемую глобальную переменную настолько трудно, что на форумах идут длинные дискуссии, как это вообще сделать. В Zig вы просто создаете ее, и всё.
Неопределенное поведение по-прежнему важно и в Zig. Там его называют «недопустимым поведением». Язык пытается обнаруживать его во время выполнения и аварийно завершать программу. Для тех, кто переживает насчет накладных расходов таких проверок, Zig предлагает четыре разных режима сборки. В некоторых из них проверки отключены. Идея, кажется, в том, что вы можете достаточно хорошо прогнать свою программу в режимах с проверками, чтобы убедиться, что в сборке без проверок недопустимого поведения не будет. На мой взгляд, это предельно прагматичное решение.
Еще одно отличие Zig от двух других языков — его отношение к объектно-ориентированному программированию. OOP давно не в моде, и Go, и Rust избегают наследования классов. Но возможностей, которые позволяют воспроизвести другие ООП-паттерны, в Go и Rust достаточно, и при желании вы можете выстроить программу как граф объектов, взаимодействующих между собой. В Zig есть методы, но нет приватных полей структур и нет механизма для реализации полиморфизма времени выполнения (динамической диспетчеризации), хотя std.mem.Allocator, кажется, буквально умоляет стать интерфейсом. Насколько я могу судить, такие исключения намеренны: Zig — язык для проектирования, ориентированного на данные.
Еще одно наблюдение, которое для меня стало откровением. Создавать язык с ручным управлением памятью в 2025 году может показаться безумием, особенно когда Rust доказал, что можно обойтись без GC и поручить все компилятору. Но эта часть дизайна тесно связана с отказом от ООП-подходов. В Go, Rust и многих других языках вы обычно выделяете маленькие кусочки памяти для каждого объекта в вашем графе объектов. Программа совершает тысячи скрытых malloc() и free(), а значит и тысячи разных времени жизни. Это RAII. В Zig ручное управление памятью кажется утомительной и опасной рутиной только если вы настаиваете на привязке выделений к каждому мелкому объекту. Но вы можете выделять и освобождать большие блоки памяти в заранее определенные разумные моменты работы программы (например, в начале каждой итерации event loop) и использовать эти блоки для данных, с которыми вам нужно работать. Именно к такому подходу Zig подталкивает.
Многие не понимают, почему Zig вообще нужен, если уже есть Rust. Дело не в том, что Zig просто стремится быть проще. Важнее другое: Zig хочет вычистить из вашего мышления еще больше объектно-ориентированных привычек.
Zig ощущается дерзким и немного подрывным. Это язык для тех, кто хочет разрушить корпоративную иерархию (объектов). Язык для мегаломанов и анархистов. Мне он нравится. Надеюсь, стабильный релиз выйдет скоро, хотя сейчас команда Zig, кажется, в приоритете переписывает все свои зависимости. Не исключено, что они возьмутся переписывать ядро Linux раньше, чем мы увидим Zig 1.0.

Друзья! Эту статью подготовила команда «Go for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Go. Подписывайтесь, чтобы быть в курсе и ничего не упустить!