Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11
Перетяжка, Премия ТПрогер, 13.11

Go против Rust против Zig: какой язык для чего нужен

Это попытка понять философию языков и определить, какой язык ближе лично вам.

60 открытий396 показов
Go против Rust против Zig: какой язык для чего нужен

Это перевод статьи для Tproger. Автор оригинала делится опытом изучения трёх системных языков программирования и размышляет, почему каждый из них сделал именно такие компромиссы в дизайне. Это попытка понять философию языков и определить, какой подход ближе лично вам.

Недавно я понял, что вместо того, чтобы использовать “правильный инструмент для задач” я просто использую инструменты, которые сказали на работе — и это определило языки программирования, которые я знаю. Последние пару месяцев я потратил много времени на эксперименты с языками, которые не использую в рабочих проектах. Цель не была в том, чтобы стать экспертом — я хотел сформировать мнение о том, для чего каждый язык действительно хорош.

Языки программирования отличаются по многим параметрам, и сравнивать их сложно, не скатываясь к совершенно скучному или бесполезному выводу “везде есть компромиссы”. Конечно, компромиссы есть всегда. Интересный вопрос — почему этот конкретный язык выбрал именно такой набор компромиссов?

Этот вопрос важен для меня, потому что я не хочу выбирать язык по чек-листу, будто покупаю увлажнитель воздуха. Меня волнует создание софта и мои инструменты. Делая свои компромиссы, языки выражают набор ценностей. Я хочу понять, какие ценности резонируют со мной.

Этот вопрос также помогает прояснить разницу между языками, которые на первый взгляд сильно пересекаются по возможностям. Судя по количеству вопросов статей вроде “Go или Rust” и “Rust или Zig”, люди тоже не понимают, что происходит. Сложно запомнить, что язык X лучше для веб-сервисов, потому что у него есть фичи a, b и c, а у языка Y только a и b. Гораздо проще запомнить, что язык X лучше для веб-сервисов, потому что язык Y создан человеком, который ненавидит интернет (условно) и считает, что нужно вырубить всю сеть.

Я собрал здесь мнение о трёх языках, с которыми недавно экспериментировал: Go, Rust и Zig. Я попытался превратить свой опыт с каждым языком в общий вывод о том, что этот язык представляет из себя и насколько хорошо реализует свои ценности. Да, это упрощение, но кристаллизация упрощённых предубеждений — именно то, что я здесь и делаю.

Go: минимализм для корпораций

Go выделяется своим минимализмом. Его называют “современным C”. Go не похож на C, потому что у него есть сборщик мусора и полноценная среда выполнения, но он похож на C тем, что весь язык помещается в голове.

Весь язык помещается в голове, потому что в Go очень мало возможностей. Долгое время Go был известен отсутствием дженериков. Их наконец добавили в Go 1.18, но только после 12 лет, в течение которых люди умоляли это сделать. Другие возможности, обычные для современных языков — например, размеченные объединения (tagged unions) или синтаксический сахар для обработки ошибок — в Go так и не появились.

Похоже, команда разработки Go ставит высокую планку для добавления новых возможностей. В результате получился язык, который заставляет писать много шаблонного кода для реализации логики, которую на другом языке можно выразить короче. Но в результате также получился язык, стабильный во времени и лёгкий для чтения.

Ещё один пример минимализма Go — тип slice. И в Rust, и в Zig есть slice, но это толстые указатели (fat pointers) и только они. В Go slice — это толстый указатель на непрерывную последовательность в памяти, но slice также может расти. То есть он объединяет функциональность типа Vec<T> из Rust и ArrayList из Zig. Кроме того, поскольку Go управляет памятью за вас, он сам решает, где будет жить память вашего slice — в стеке или куче. В Rust или Zig вам придётся сильно задуматься, где живёт ваша память.

История происхождения Go, насколько я понимаю, примерно такая: Роб Пайк устал ждать компиляции проектов на C++ и устал от ошибок, которые другие программисты Google делали в тех же проектах на C++. Поэтому Go прост там, где C++ перегружен. Это язык для рядовых программистов, спроектированный быть достаточным для 90% задач и при этом простым для понимания, даже (или особенно) при написании конкурентного кода.

Я не использую Go на работе, но думаю, что должен бы. Go минималистичен ради корпоративного сотрудничества. Я не считаю это недостатком — создание софта в корпоративной среде имеет свои вызовы, которые Go решает.

Rust: максимализм ради безопасности

Если Go минималистичен, то Rust максималистичен. Популярный слоган Rust — “абстракции с нулевой стоимостью” (zero-cost abstractions). Я бы дополнил: “абстракции с нулевой стоимостью, и их очень много!”

У Rust репутация сложного для изучения языка. Я согласен с Джейми Брэндоном, который пишет, что Rust делает сложным не система времён жизни (lifetimes), а количество концепций, запиханных в язык. Я не первый, кто приводит в пример этот конкретный комментарий на GitHub, но он идеально показывает концептуальную плотность Rust:

Тип Pin<&LocalType> реализует Deref, но не реализует DerefMut. Типы Pin и & помечены #[fundamental], так что возможна реализация DerefMut для Pin<&LocalType>>. Вы можете использовать LocalType == SomeLocalStruct или LocalType == dyn LocalTrait, и можете привести Pin> к Pin>. (Действительно, два слоя Pin!!) Это позволяет создать пару “умных указателей, реализующих CoerceUnsized, но имеющих странное поведение” на стабильной версии (Pin<&SomeLocalStruct> и Pin<&dyn LocalTrait> становятся умными указателями со «странным поведением», и они уже реализуют CoerceUnsized).

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

Цель производительности понятна сама по себе. Что означает “безопасность” — менее очевидно, по крайней мере для меня (хотя, может быть, я просто слишком долго писал на Python). “Безопасность” означает “безопасность памяти” — идею, что вы не должны иметь возможность разыменовать невалидный указатель или сделать двойное освобождение памяти. Но это также означает больше. “Безопасная” программа избегает всего неопределённого поведения (undefined behavior, или UB).

Что такое ужасное UB? Лучший способ понять это — вспомнить, что для любой работающей программы ЕСТЬ СУДЬБЫ ХУЖЕ СМЕРТИ. Если в программе что-то идёт не так, немедленное завершение — это прекрасно! Потому что альтернатива, если ошибка не поймана — ваша программа переходит в сумеречную зону непредсказуемости, где её поведение может определяться тем, какой поток выиграет следующую гонку данных, или тем, какой мусор оказался по конкретному адресу памяти. Теперь у вас хайзенбаги и дыры в безопасности. Очень плохо.

Rust пытается предотвратить UB без потери производительности во время выполнения, проверяя всё во время компиляции. Компилятор Rust умный, но не всеведущий. Чтобы проверить ваш код, ему нужно понимать, что код будет делать во время выполнения. Поэтому в Rust есть выразительная система типов и множество трейтов, которые позволяют объяснить компилятору то, что в другом языке было бы просто видимым поведением кода во время работы.

Это делает Rust сложным, потому что вы не можете просто взять и сделать что-то! Вы должны узнать, как Rust это называет — найти нужный трейт или что-то ещё — и реализовать это так, как Rust ожидает. Но если вы это делаете, Rust может дать гарантии о поведении вашего кода, которые другие языки не дают, а это в зависимости от приложения может быть критично. Он также может давать гарантии о чужом коде, что делает использование библиотек в Rust простым и объясняет, почему проекты на Rust имеют почти столько же зависимостей, сколько проекты в экосистеме JavaScript.

Zig: свобода и контроль

Из трёх языков Zig самый новый и наименее зрелый. На момент написания статьи Zig на версии 0.14. У его стандартной библиотеки почти нет документации, и лучший способ научиться её использовать — читать исходный код напрямую.

Не знаю, правда ли это, но мне нравится думать о Zig как о реакции одновременно на Go и Rust. Go прост, потому что скрывает детали того, как работает компьютер. Rust безопасен, потому что заставляет прыгать через свои обручи. Zig освободит вас! В Zig вы контролируете вселенную, и никто не может указывать, что делать.

И в Go, и в Rust выделить объект в куче просто — достаточно вернуть указатель на структуру из функции. Выделение памяти неявное. В Zig вы выделяете каждый байт сами, явно. (В Zig ручное управление памятью.) У вас больше контроля, чем даже в C: чтобы выделить байты, нужно вызвать alloc() на конкретном виде аллокатора, то есть вы должны выбрать лучшую реализацию аллокатора для вашего случая.

В Rust создать изменяемую глобальную переменную настолько сложно, что на форумах идут длинные обсуждения, как это сделать. В Zig вы просто создаёте её, без проблем.

Неопределённое поведение всё ещё важно в Zig

Zig называет его “нелегальным поведением” (illegal behavior). Он пытается обнаружить его во время выполнения и обрушить программу, когда это происходит. Для тех, кого беспокоит стоимость таких проверок по производительности, Zig предлагает четыре разных “режима релиза” на выбор при сборке программы. В некоторых проверки отключены. Идея в том, что вы можете запустить программу достаточно раз в проверяемых режимах, чтобы иметь разумную уверенность: в непроверяемой сборке нелегального поведения не будет. Это кажется мне очень прагматичным дизайном.

Ещё одно различие между Zig и двумя другими языками — отношение Zig к объектно-ориентированному программированию. ООП давно не в моде, и Go, и Rust избегают наследования классов. Но в Go и Rust достаточно поддержки других идиом ООП, чтобы вы могли построить программу как граф взаимодействующих объектов, если захотите. В Zig есть методы, но нет приватных полей структур и нет языковой возможности для полиморфизма во время выполнения (динамической диспетчеризации), хотя std.mem.Allocator просто умирает стать интерфейсом. Насколько я могу судить, эти исключения намеренны; Zig — язык для дата-ориентированного дизайна (data-oriented design).

Ещё одна вещь, которую я хочу сказать, потому что она открыла мне глаза: может показаться безумием создавать язык программирования с ручным управлением памятью в 2025 году, особенно когда Rust показал, что сборка мусора не нужна и компилятор может всё сделать за вас. Но это дизайнерский выбор, тесно связанный с выбором исключить возможности ООП. В Go, Rust и множестве других языков вы обычно выделяете маленькие кусочки памяти за раз для каждого объекта в графе объектов. У вашей программы тысячи маленьких скрытых malloc() и free(), и, следовательно, тысячи разных времён жизни. Это RAII. В Zig может показаться, что ручное управление памятью потребует много утомительной, подверженной ошибкам работы, но это так только если вы настаиваете на привязке выделений памяти к каждому маленькому объекту. Вместо этого вы можете просто выделять и освобождать большие куски памяти в определённых разумных точках программы (например, в начале каждой итерации цикла событий) и использовать эту память для данных, с которыми работаете. Именно этот подход и поощряет Zig.

Многие люди не понимают, зачем нужен Zig, если уже есть Rust. Дело не только в том, что Zig пытается быть проще. Думаю, разница более важная. Zig хочет, чтобы вы вырезали ещё больше объектно-ориентированного мышления из своего кода.

У Zig весёлая, подрывная атмосфера. Это язык для разрушения корпоративной классовой иерархии (объектов). Это язык для мегаломанов и анархистов. Мне он нравится. Надеюсь, он скоро выйдет в стабильный релиз, хотя текущий приоритет команды Zig — переписать все свои зависимости. Не исключено, что они попытаются переписать ядро Linux, прежде чем мы увидим Zig 1.0.

Go — для командной работы и быстрой разработки, Rust — для критически важных систем, где нужны максимальные гарантии, Zig — для тех, кто хочет полного контроля и готов от ООП отказаться в пользу дата-ориентированного подхода. Выбор зависит не от списка фич, а от того, какая философия вам ближе.

Следите за новыми постами
Следите за новыми постами по любимым темам
60 открытий396 показов