Как стать автором
Обновить

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

В плане шаблоны? Немного не понял. Может имелись в виду дженерики?

Я вот тоже подозреваю, что автору не нравятся дженерики.

Только вот они на любом языке будут довольно многословные, просто потому что это так работает.

Обобщения они зовутся по-русски, а так да, generics.

Шаблонами это называется в С++. С тех пор и в других языках называю это шаблонами.

Да не называется "это" шаблонами в C++. Идея шаблонов в C++ основана на duck typing через инстанцирование шаблона "по месту", в то время как идея дженериков основана на "низведении" передаваемых типов к некоему общему знаменателю через type erasure в том или ином виде. Разница принципиальная. Например, в C++ вы можете сделать так:

auto mul(auto a, auto b)
{
    return a * b;
}

mul(1, 2); // Инстанцируется int mul(int, int)
mul(1.1, 2); // Инстанцируется double mul(double, int)
mul(1, "a"); // Ошибка, нет подходящего operator*

в то время, как ни в Go, ни в Rust ничего подобного сделать нельзя, нужно в дженерике явно указывать ограничения для типов:

// Не скомпилируется без наложения ограничений на T через указание
// соответствующего трейта
fn mul<T>(a: T, b: T) -> T
{
    a * b
}
// Тоже не скомпилируется без наложения соответствующих ограничений на T
func mul[T any](a T, b T) T {
	return a * b
}

Дженерики не всегда сделаны через type erasure.

Я написал "в том или ином виде", переданный тип внутри дженерика по сути раскладывается на набор неких трейтов (или, если угодно, интерфейсов), с которыми можно делать то, что делается с этим типом внутри дженерика, и этот набор трейтов/интерфейсов нужно прописывать руками.

Насколько мне известно, в го применяются разные подходы в случае с функциями и в случае с типами. Для функций предусмотрен механизм «шаблонных функций», типа как в плюсах: компилятор находит все варианты использования и генерирует несколько вариантов для всех задействованных — потому они не дают оверхеда. А вот для типов на этапе компиляции проводятся лишь поверхностные проверки совместимости, а в рантайме начинается цирк с приведениями, что бьёт по производительности.

Ну и нет нужды явно указывать тип в момент вызова. Если компилятор может догадаться, то он это сделает. Да, ограничения накладывать нужно при определении типа или функции — для выявления допустимых операций над типом. По сути это упрощение поиска ошибок типа «нет подходящего operator*», но только заложенное в сам дженерик, а не в место вызова.

Эта статья была очень интересной!

Для разработки библиотек шаблоны удобны - писать меньше. А вот для конечных программистов - дело подготовки. Зачем нам ещё один Раст - не понятно. Уж лучше бы обработку ошибок сделали удобнее и лаконичнее, как в Zig, например.

А как в Zig сделано?
Просто я знаю три основных больших способа:
1. Коды ошибок, которые можно игнорировать (было сделано в Go изначально, хоть и не на столько плохо, как в си)
2. В функциональном стиле через Result или Either (сделано было в Rust и хочется видеть в Go)
3. Через исключения (чего не хочется теперь видеть нигде, но оно уже есть во многих языках)

А чем исключения плохи можете пояснить? если не говорить о перфомансе.
Я вот ни как не могу принять эти "новые концепции обработки ошибок". Все эти проверки кодов ответа кажутся откатом к темным временам.

Все эти проверки кодов ответа кажутся откатом к темным временам.

Вам не кажется, это он и есть.

Обычно инх никто не проверят. Есть целая религия непроверки результата и errno (а это тот еще ктулху). Поэтому им нравится такой метод сообщения об ошибках.

Исключения плохи либо тем, что они многословные (как в случае с checked exception в java), либо тем что они являются неявным способом возврата значений.

И при этом сложно определить, когда исключение является обычной ошибкой, которую надо обработать, а когда - непредвиденной ситуацией, по которой нужно аварийно всё завершать.

По тому я считаю наиболее оптимальным решение с обработкой ошибок через паники (когда ситуация аварийная) и Result (когда ошибка предвиденная)

И при этом сложно определить, когда исключение является обычной ошибкой, которую надо обработать, а когда - непредвиденной ситуацией, по которой нужно аварийно всё завершать.

Это легко решается создание своего класса исключения (прикладного) и наследования от него всей нужной иерархии и их обработки. При этом код не засоряется всем этим мусором бесконечным.

Как практик с 20 летним стажем, к сожалению я не понял что вы хотели сказать. Громоздкость зависит от языка. Не вижу громоздкости например в Руби. По не явному возврату значения тоже не понятно. Почему объект исключения не может содержать ссылки или значения дополнительные? Ну и в нормальном языке (например Руби) тебе никто не мешает создать объект / метод у которого будет результат, в том числе говорящей о ошибке и возвращающей доп. инфу.

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

Экономить нужно когнитивный ресурс человека (прежде всего того который будет годами читать этот код).

А ещё исключения очень дорогие и не позволяют выразить идею "эта функция ни при каких условиях не завершится с ошибкой".

А громоздкость обработки ошибок через Result решается через комбинаторы/оператор вопросительного знака/do-notation.

А как сделать свой доменный класс для ошибок и сделать так, чтобы ошибки из сторонних библиотек его наследовали?)

А ещё исключения очень дорогие и не позволяют выразить идею "эта функция ни при каких условиях не завершится с ошибкой".


Видимо я не понял проблему. Перехвати все стандартные (не системные) исключения вот и все.



> А громоздкость обработки ошибок через Result решается через комбинаторы/оператор вопросительного знака/do-notation.

Покажите мне пожалуйста красивый, не мусорный код на го где этак концепция применяется и не нужно после каждого вызова явно проверять код ответа.

> А как сделать свой доменный класс для ошибок и сделать так, чтобы ошибки из сторонних библиотек его наследовали?)

На руби это решается выносом базовых классов в базовую библиотеку (гем) или путем калбеков - одна библиотека выставляет калбек в котором ты в приложении можешь что-то проинициализировать (создать экземпляр нужного исключения) или на уровне конфигурации - передача прикладного класса исключения в приложении как опции для библиотеки.

Покажите мне пожалуйста красивый, не мусорный код на го где этак концепция применяется и не нужно после каждого вызова явно проверять код ответа.

А в Go такого ещё нет, на сколько я знаю)

Описывал, как это в целом уже решено в других языках:

Rust

fn my_function() -> Result<(), MyError> {
  let a = may_fail1()?;
  let b = may_fail2(a)?;
  let c = may_fail3(b)?;
  will_not_fail(c);
  Ok(())
}

F#

let my_function () = result {
  let! a = may_fail1 ()
  let! b = may_fail2 a
  let! c = may_fail3 b
  do! may_fail4 c
  will_not_fail ()
  ()
}

На руби это решается ...

  1. Это на руби. Можно пример кода из других языков?

  2. Это не выглядит как что-то дешёвое. Result по вычислительным ресурсам и памяти часто не дороже, чем сишные коды ошибок.

Видимо я не понял проблему. Перехвати все стандартные (не системные) исключения вот и все.

Как раз в этой необходимости перехватывать там, где это не нужно)
А ещё в том, что компилятор, имея информацию о том, что исключения никогда не будут выкинуты - может неплохо оптимизировать часть кода.


как сделать свой доменный класс для ошибок и сделать так, чтобы ошибки из сторонних библиотек его наследовали?

Перехватывать стронние исключения и заворачивать их в свои типы. Чтобы этот мусор не мешал читать алгоритм - библиотеку можно спрятать под прокси/фасад.

Получается мы берём технологию, которая создавалась для более лаконичной обработки ошибок (исключения) и полностью списываем её главное заявленное преимущество, обмазывая вызовы сторонних библиотек своими исключениями и фасадами?)

И при этом сохраняем все недостатки в виде нелинейности и дороговизны в рантайме.

Получается, что даже если хотеть странного, то можно найти способ сохранить код основного алгоритма в чистоте.

не позволяют выразить идею "эта функция ни при каких условиях не завершится с ошибкой"

Если вы имеете в виду "эта функция ни при каких условиях не выбросит исключение" то отчего же, вполне себе позволяют. В C++ например для декларирования этого можно добавить к сигнатуре функции спецификатор noexcept.

А во всех ли языках с исключениями имеется такая аннотация?

Да и обычно кажется, что функций, которые могут аварийно завершиться меньше, чем тех, которые аварийно не завершаются.

Ну и всё ещё не понятно, как отличить аварийную ситуацию (нарушение каких-то инвариантов, на которых вся система строится) от просто ошибки (нет доступа к файлу или не получается спарсить число).
Подход с Result и паниками как раз позволяет логично такое разделение показать.

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

Подход с Result и паниками как раз позволяет логично такое разделение показать.

Паника - это жалкое подобие левой руки от раста, которую пытаются применять для эмуляции тех самых исключений, которые якобы "не нужны". Не просто так пытаются лепить всякие catch_unwind.

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

А эта проблема и вовсе существует только в вашей голове. Ладно, не интересно.

Основной смысл catch_unwind - поймать панику до того, как оно пересечёт FFI, так как в противном случае это будет UB.

То, что этот `catch_unwind` лепят где не попадя - так это не проблема раста. В документации к этой функции, кстати, прямо сказано: использовать её для реализации механизма try\catch - моветон.

То, что этот `catch_unwind` лепят где не попадя - так это не проблема раста.

А чья эта проблема? Очевидно же, что есть причина для того, что люди лепят его "где ни попадя".

В документации к этой функции, кстати, прямо сказано: использовать её для реализации механизма try\catch - моветон.

Тем не менее такая практика распространена достаточно широко для того, чтобы о ней не поленились упомянуть в официальной документации, пусть и в стиле "not recommended". И не от хорошей жизни.

А чья эта проблема?

Ну уж точно не раста. Не виноват он в том, что людям удобно тащить свои знания, опыт и привычки из других языков. Хотите использовать исключения - идите в кресты и не мучайте себя растом.

Тем не менее такая практика распространена достаточно широко для того, чтобы о ней не поленились упомянуть в официальной документации, пусть и в стиле "not recommended".

Такая формулировка была с момента появления/стабилизации данной функции, так что нет. Или может есть пруфы, что catch_unwind действительно лепят везде?

Знаете, очень забавно, что именно вы сейчас мне это пишете, ибо совсем недавно именно вы мне здесь доказывали, что "хотя в раст (и го) сообществе принято говорить, что мол исключений в языке нет, но так-то паника не особо от них отличается: можно перехватить, узнать тип, пробросить дальше", то есть фактически вы рекламировали панику как некий аналог исключений. Вы уж определитесь. Ладно, нет смысла снова идти по N-ному кругу, а к двоемыслию растаманов в рекламе их любимого язычка в зависимости от ситуации я давно привык. Это двоемыслие, кстати, вызвано причинами чисто догматического характера: с одной стороны, идеология раста вдалбливает вам, что "исключения - это плохо потому, что потому", а с другой стороны, крутиться как-то надо. В общем, фанатикам, у которых реальность расходится с их догмами, можно только посочувствовать. Вот тут человек выше рекомендовал мне "не мучать себя растом", что ж, я так и делаю, и только рад этому.

то есть фактически вы рекламировали панику как некий аналог исключений.

Нет, не рекламировал. Я и сейчас считаю, что когда говорят, что в Rust или Go нет исключений, то немного лукавят. В техническом плане паника - это те самые исключения. Разве что немного менее удобные так как сахара для обработки не завезли. Честнее было бы говорить, что строить логику обработки ошибок на паниках не принято. И что характерно рекомендации работают. По крайней мере, я не наблюдаю обратного.

с одной стороны, идеология раста вдалбливает вам, что "исключения - это плохо потому, что потому", а с другой стороны, крутиться как-то надо

Ну так можно показать эту другую сторону? Ту самую, где двуличные растоманы повсеместно используют панику как исключения.

человек выше рекомендовал мне "не мучать себя растом", что ж, я так и делаю, и только рад этому.

Осталось сделать следующий шаг и не читать темы про раст. Может быть тогда фанатики перестанут мерещиться.

Ну так можно показать эту другую сторону? Ту самую, где двуличные растоманы повсеместно используют панику как исключения.

Вам ссылку на ваш комментарий еще раз прислать? :) Или вы придрались к слову "повсеместно"? Так это вы его употребили, а не я. Я лишь написал, что такая практика встречается. "Повсеместно" народ пишет бойлерплейты по прокидыванию наверх Result'ов, но я вполне понимаю, что это получается далеко не всегда. Кстати, лично я тоже не приравниваю паники к исключениям, поэтому я и упомянул про "жалкое подобие левой руки".

Осталось сделать следующий шаг и не читать темы про раст. Может быть тогда фанатики перестанут мерещиться.

Я всего лишь возразил на ложное утверждение гражданина выше, что, мол, исключения не позволяют выразить идею "эта функция ни при каких условиях не завершится с ошибкой", на примере C++, но он прямо на ходу начал изобретать новые причины, почему исключения - это плохо. Но ведь реально от упорства растаманов в доказывании того, что исключения "не нужны" просто потому, что в их любимом языке их нет, веет чем-то нездоровым. Казалось бы, что плохого в том, чтобы иметь выбор? Кстати, по теме статьи это мне напоминает, как не столь давно Go'шники доказывали, что дженерики не нужны, я помню, читал всякие баттлы и статьи на эту тему... А потом дженерики добавили в язык, и довольных этим в конечном счете оказалось больше, чем недовольных.

Вам ссылку на ваш комментарий еще раз прислать?

И где там такое? Ещё раз: я про техническую возможность. Да, она есть. Точно так же как технически можно сделать С++ либу с ошибками через подобие errno/GetLastError. И не удивлюсь если даже такие примеры можно найти, но утверждать, что это распространённая практика, которую кое-как сдерживают рекомендации будет весьма странно.

Или вы придрались к слову "повсеместно"?

Нет, я придрался к "такая практика распространена достаточно широко". И то не придрался, а поинтересовался есть ли повод для таких утверждений или это просто гадания основанные на формулировке из документации.

"Повсеместно" народ пишет бойлерплейты по прокидыванию наверх Result'ов

Что ж, я рад, что в этом мы согласны. Ну с поправкой, что все эти ужасные бойлерплейты как правило состоят аж целого вопросительного знака. Ок, нескольких вопросительных знаков по стеку вызовов.

Казалось бы, что плохого в том, чтобы иметь выбор?

Разве это не очевидно? Фрагментация инфраструктуры и неудобства при интеграции библиотек, авторы которых сделали выбор противоположный нашему. Если что, я не собираюсь спорить, что как возможность выбирать в целом, так и исключения в частности имеют свои достоинства.

А потом дженерики добавили в язык, и довольных этим в конечном счете оказалось больше, чем недовольных.

Опять же, любопытно есть ли статистика? И можно ли в принципе её объективно собрать... Так-то мне периодически попадались статьи и жалобы, вот например из недавнего на хабре. Да и что могут сделать недовольные? Максимум не использовать дженерики в своём коде, не форкнуть же язык в самом деле.

И не удивлюсь если даже такие примеры можно найти, но утверждать, что это распространённая практика, которую кое-как сдерживают рекомендации будет весьма странно.

Ну почему же, растаманы прокидывают свои Result'ы, стараются. Но вот например авторы salsa решили для своих отменяемых запросов применять подход, сильно напоминающий использование исключений - неужели "ниасилили" сделать все по "правильным" догмам? Да нет, не может быть. И я подобное видел неоднократно, сейчас просто лень копаться. И это уж не говоря о ситуациях, когда ошибки и не стараются прокидывать наверх вовсе, а просто пишут unwrap().

Ну с поправкой, что все эти ужасные бойлерплейты как правило состоят аж целого вопросительного знака.

Это пока вам не нужно преобразовать один "не ваш" тип, входящий в состав Result в другой "не ваш" тип для его дальнейшего проброса наверх. Ну и если не считать того, что преимущества проверок результата на всех уровнях каждый божий вызов по сравнению с исключениями сомнительны для обработки таких ошибок, которые по своей сути происходят редко.

Так-то мне периодически попадались статьи и жалобы, вот например из недавнего на хабре.

Хех, ну судя по рейтингу статьи и каментам, читатель не впечатлился :) Так что так себе пример.

Но вот например авторы salsa решили для своих отменяемых запросов применять подход, сильно напоминающий использование исключений

Любопытный пример, на досуге изучу подробнее. Но пока остаюсь при своём мнении, что это всё же редкость.

И это уж не говоря о ситуациях, когда ошибки и не стараются прокидывать наверх вовсе, а просто пишут unwrap().

В определённых случаях это вполне уместно.

Это пока вам не нужно преобразовать один "не ваш" тип, входящий в состав Result в другой "не ваш" тип для его дальнейшего проброса наверх.

Если преобразовывать надо в чужой тип, значит на это полагается какой-то внешний код. То есть, преобразование и с исключениями нужно будет. И в этом случае бойлерлейта будет даже больше:

foo().map_err(|e| AnotherError(...))?;
try {
    foo();
} catch(SomeException e) {
    throw AnotherException(...);
}

Хех, ну судя по рейтингу статьи и каментам, читатель не впечатлился :) Так что так себе пример.

Только сейчас заметил, что дал ссылку на статью в комментариях к которой мы и находимся. ? Переходил к обсуждению из почты, так что не обратил на это внимание. Но вообще подобное нытья я тоже видел неоднократно.

Если преобразовывать надо в чужой тип, значит на это полагается какой-то внешний код.

Скажем так, это вещи не связанные. Внешний код может исключения вовсе не обрабатывать, а позволять это делать твоему коду, который этот внешний код вызывает. Просто в случае раста тут нет никакого выбора: хочешь ты или не хочешь, а преобразование нужно, чтобы внешний код потом вернул ошибку тебе (причем не факт, что есть вообще способ адекватно преобразовать одну ошибку в другую с сохранением ее смысла и метаданных). В случае же исключений ты просто ловишь в своем коде изначальную ошибку со всеми метаданными.

А чем исключения плохи можете пояснить?

  1. В Go ошибки обрабатываются явным образом, что позволяет легко отслеживать ошибки в коде. Это делает код более понятным и предотвращает непредвиденное поведение.

  2. В случае возникновения ошибки, Go требует, чтобы они были обработаны явно. Это позволяет программисту контролировать, как и когда каждая ошибка обрабатывается.

  3. Нет неявного перехвата ошибок.

  4. Возможность комбинирования ошибок

  5. Подход обработки ошибок в Go прост в использовании и не внесет дополнительной сложности в код. Он также улучшает производительность, поскольку нет необходимости в неэффективной работе с исключениями и стеком вызовов.

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

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

Уж лучше многословные шаблоны, чем interface{} на каждый чих. При том что на практике обе этих крайности почти всегда неправильные (и в Rust, когда ты сам пишешь код, а не отдаёшь его кодогенератору, можно и нужно вовремя вставить Box<dyn Something>, и в Go вполне можно многие вещи сделать более-менее типобезопасными).

Назвать GORM основной библиотекой, да еще и отличной, да еще и понятной - это сильно. По мне так использовать ее можно только тогда, когда ты совершенно точно знаешь, зачем ты ее используешь, какие профиты из этого получишь, и четко представляешь, в какие проблемы это выльется по мере развития проекта.

Какую ORM вы посоветуете для Go ?

Слышал мнение, что в Go просто не нужны ORM. Можно же просто сырой SQL использовать

От CRM мне нужна поддержка подключения к разным СУБД через одно API и мапинг данных из строки БД на структуру. Сами запросы я предпочитаю писать на SQL.

Тогда есть sqlx - как раз ровно оно: общий API для управления транзакциями и подключения, но при этом запросы пишутся на SQL.

Тогда это не orm, а просто дата маппер. Sqlx подойдёт, но ещё можно посмотреть на sqlboiler или sqlc.

На Rust это не быстрая ORM, для Go навскидку нашел пару сравнений производительности https://blog.jetbrains.com/go/2023/04/27/comparing-db-packages/ и https://github.com/efectn/go-orm-benchmarks/blob/master/results.md, но результаты почему то в них разные. Когда в свое время выбирал ORM, Gorm был вроде один из самых быстрых.

Пока у вас в меру простые запросы все хорошо. А потом будете бороться с GORM. SQL запрос вроде как написан и оттестирован, но уложить его в GORM... бывает вызвает проблему. Может что-то и изменилось за 2 года, но я зарекся с ним больше не связываться

Я могу сразу написать нужные модели и репозитории и не тянуть GORM). Пару заготовок есть и времени на создание много времени у меня не занимает. Каждому свое. Вам удобно - пользуйтесь на здоровье.

На Rust это не быстрая ORM

Что значит "это"? Гошный sqlx и растовый sqlx - это две абсолютно разные библиотеки.

И что значит "не быстрая"?

И что значит "не быстрая"?

Значит медленнее чем ряд других ОRМ, например медленнее чем Diesel

  1. Строго говоря sqlx не orm.

  2. Можно бенчмарк увидеть? Звучит странно, с учётом того что sqlx делает гораздо меньше работы, чем дизель.

  3. С дизелем сравнивать не очень корректно, тк он не умеет в асинхронщину => даже если в полностью синхронном коде он выдаёт более высокую производительность - не факт, что он будет давать то же самое в каком-нибудь высоконагруженном приложении.

UPD: Нашёл https://github.com/diesel-rs/metrics/ - действительно интересно. На батчах 1000+ строк действительно дизель (и diesel-async) быстрее

Не разделяю опасения.

В Go ввели шаблоны и он потерял одно из своих основных преимуществ: легкую читаемость.

Читаемость можно ухудшить множеством способов. Как вам, например, описание цепочки функций высшего порядка?

func MyfuncA(param1, param2 string) (func (param3 string, param4 func (param5 *interface{}, param6 int64) func () int64) func (param5 *interface{}) int64, error)

Что бы сделать например обобщённую функцию для sqlx нужно описать портянку из параметров шаблонов [...] Но ведь в Go например есть GORM, который без всяких шаблонов делает это.

И он это делает с помощью рефлексии в рантайме, когда шаблоны разворачиваются в куда более простой и прямолинейный код при компиляции. Разница в производительности просто гигантская. Впрочем, гошные дженерики реализованы не так, как многие надеялись: без полной мономорфизации, чтобы сохранить быструю компиляцию, так что в рантайме все же довольно много всего происходит.

А в реальных проектах с кучей библиотек будут огромные строки указания параметров шаблона.

Дженерики в go не полные по Тьюрингу, так что творить на них какую-то чудовищую магию, как это возможно, например, в плюсах, попросту не получится. Их введение прошло с очень длительными и обстоятельными обсуждениями, главный мотив в которых был "не переусложнить язык". На мой взгляд, получилось неплохо: все дженерики, которые я писал или видел, были очень простыми и всего лишь способствовали универсальности функций или контейнеров при сохранении типизации, так что область их применения невелика; никакого SFINAE тут нет, к счастью. К тому же, экосистема уже достаточно крупная и устоявшаяся, чтобы появление дженериков не перевернуло все с ног на голову. Собственно, такого и не происходит, хотя дженерикам уже полтора года.

Как же обходились без шаблонов в Go 13 лет?

Иногда с недовольством недостаточной выразительностью языка и необходимостью вручную писать одни и те же вещи из раза в раз вроде общих алгоритмов для массивов, которые теперь появились в пакете slices; кодогенерация как предлагаемая альтернатива не особо помогала. Собственные контейнеры всегда приходилось затачивать под конкретные типы, их толком не вынести было в библиотеки. Часто для обобщения приходилось уничтожать информацию о типе через пустой интерфейс, а потом ее восстанавливать через попытки приведения: уж лучше дженерики.

Раз теперь код в Go будет выглядеть как в Rust, то зачем продолжать использовать Go?

А зачем нужен был раст, если в нем есть такие же шаблоны, как в уже существующих плюсах? Языки все же отличаются не только синтаксисом, да и обобщение уж слишком натянутое. Go все так же будет иметь намного более низкий порог входа и простоту поддержки легаси, все так же будет управлять сбором мусора, все так же будет хорош легковесными горутинами и т.д.

Согласен с вашими высказываниями. Насчет языков мне кажется у Go и у Rust одинаковые концепции, они близки по подходам к разработке (через интерфейсы/трейты, модули). Rust решил проблему утечек памяти и рантайм ошибок, Go обеспечил быстрое обучение + быстрая разработка мелких программ. Оба языка обеспечивает небольшое использование памяти программами и отличное быстродействие. Но лично мне не нравится в Go после вызова каждой функции (которая возвращает ошибку) писать обработку ошибки.

Ну изначально Rust зарождался как второй Go с гринтредами и сборкой мусора, но потом ушёл в сторону безопасности во время компиляции и zero cost.

Лол, вообще ни разу. Раст начинался как пет проект внутри мозиллы сильно раньше и был анонсирован на пару лет раньше го.

И уж тем более, раст никогда не позиционировался, как "второй го". Он всегда был "более безопасным С++".

Раст начинался ещё до мозиллы (вернее до того как мозилла начала его финансировать)

"Второй go" - это мой термин, который я придумал, глядя на набор фичей (2010-2014 год):

  1. "GC pointers" через @

  2. Гринтреды: ключевое слово task

  3. Каналы: ключевое слово chan

И чем это не второй го?

Всё это даже в переводе на Хабре есть.

Но лично мне не нравится в Go после вызова каждой функции (которая возвращает ошибку) писать обработку ошибки.

Попробуйте приучить себя расширять контекст ошибки при прокидывании.

Например:

if err := some.Sum(a, b); err != nil {
  return fmt.Errorf("couldn't summarize %d and %d: %w", a, b, err)
}

Вам это здорово поможет при расследовании инцидентов и отладке.

Про то что это неправильно называть шаблонами уже сказали.
В остальном - на мой взгляд женерики - это точно хорошо, а не плохо.
Без них просто нельзя было бы сохраняя строгую статическую типизацию описывать обобщённые структуры, как это было в примере кода на Rust.
> Вот пример типа из Rust (с первого раза же понятно, что это за тип? сарказм ):

Ага, было бы гораздо понятнее, если бы был interface {} или какой-нибудь Any или Object, обмазанный рефлексией в рантайме.

Там где обобщения не нужны - их можно просто не использовать.

На нем можно писать под web (backend+frontend, wasm), Android, iOS, UI приложения для Windows, Linux, MacOS ?

Backend можно и даже очень хорошо. на счет UI не уверен. Не все сразу. Развивайте, двигайте. Я вот доначу регулярно уже много лет.

https://github.com/hugopl/gtk4.cr

А в чём профит? Даже разработчики языка не могут у себя на главной показать своё УТП.
Go - Очень простой, поддерживается всеми большими корпорациями, заточен под concurrency из коробки.
Rust - Производительный, надёжный, безопасный
Crystal - Руби со статической типизацией и гринтредами?

На главной же написано язык для людей. Тем не менее и для компьютера то есть быстрый.

Язык для людей - очень пространное описание. Синтаксис COBOL сделали многословным тоже под этим предлогом. Я не к тому, что мне crystal не нравится, а к тому что "для людей" не может быть конкретной целью, потому что разным людям разные вещи нравятся

Звучит скорее как лозунг, чем как преимущество.

Go без дженериков использовать было очень неудобно, сейчас он, хотя бы, стал чуть удобнее. Ещё бы дженерики были полноценные, чтобы можно было у "метода" использовать не только те типы, которые объявлены у родительского struct/interface

Вы считаете, что в примере из статьи дженерики улучшили код? Этот код легко прочитать и понять?

Вы считаете, что ваш пример является хорошим? Приведите пример, где использование дженериков оправдано, например в контейнерах, покажите, что с ними код стал хуже, и покажите, что без них он становится лучше. А то я могу доказать, что оператор сложения делает код хуже:

int main(void) {
    // умножим пять на пять
    int result = 5 + 5 + 5 + 5 + 5; // ох, уж этот плюсик
}

А теперь сделаем без сложения

int main(void) {
    // умножим пять на пять
    int result = 5 * 5; // Видите, плюсик - зло
}

Вот таким нехитрым образом можно доказать, что оператор сложения - зло

Благо это или зло для разработки?

Ни то, ни другое, это просто инструмент обобщенного программирования. А вот как этим уже будут пользоваться – другой вопрос. В целом писать трудночитаемый (и труднопонимаемый в том числе) код можно и без дженериков, шаблонов и макросов.

По факту использую дженерики в 2% кода максимум и то уже после рефакторинга, когда становистя понятно что какие-то функции можно переписать на дженерики просто для уменьшения кода.

Тоже опасался что они будут везде, но нет, это не так.

Вот пример типа из Rust (с первого раза же понятно, что это за тип? сарказм )

Если код отформатировать по-человечески и отбросить имена модулей импортами, то в коде отчетливо начнет читаться нечто похожее на полную форму select-запроса.

Код
use diesel::query_builder::{
    select_clause::SelectClause,
    distinct_clause::NoDistinctClause,
    where_clause::WhereClause,
    order_clause::NoOrderClause,
};
use diesel::expression::{
    select_by::SelectBy,
    grouped::Grouped,
    operators::Eq,
    bound::Bound,
};
use schema::users;

SelectStatement<
    FromClause<users::table>,
    SelectClause<SelectBy<User, _>>,
    NoDistinctClause,
    WhereClause<
        Grouped<
            Eq<
                users::columns::username,
                users::columns::username>>>,
    NoOrderClause,
    LimitOffsetClause<
        LimitClause<Bound<BigInt, i64>>,
        NoOffsetClause>>

Это он ещё не знает что есть type alias и type alias impl trait?

Как-то не тянет на статью

Продолжая логику: а вот раньше на C вообще без классов писали, а теперь нагромоздили всего.

Кстати, классы в Rust есть. Просто они type classes, что (на мой вкус) является очень правильной реализацией ООП.

Rust вообще получился в том числе очень неплохой проработкой родовых травм других языков - наследования, нуллов, исключений.

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

Логика статьи не в том, что дженирики плохо, а в том, что если в Go превращается в Rust, то почему бы сразу не писать на Rust?

В расте тоже есть дженерики, почему бы не писать на c++, haskell, etc. ведь в них тоже есть дженерики, а значит раст превращается в них, зачем на нём писать? Раст от го не только наличием дженериков отличается, поэтому утверждение, что го превращается в раст некорректно.

Мне пока видится, что Go это подмножество Rust. У того же С++ другая концепция (ООП, классы, ручной контроль за памятью). Что есть в Go, чего нет в Rust? Могу предположить, что только концепция каналов. (GC в расчет не берем, т.к. в Rust он не нужен и в этом одно из его преимуществ). Никто из комментаторов ни приводит ни одного довода за или против Go/Rust, все суждения не объективны. Я пересмотрел кучу статей "сравнений" этих двух языков и везде только общие слова, как будто написанные GPT, никакой конкретики, примеров.

Что есть в Go, чего нет в Rust?

GC. Да, как вы сказали, в расте он не нужен, но из-за этого возникают сложности с написанием кода. Собственно, поэтому я иногда использую го, но никогда не захочу использовать раст. Если мне нужен язык без gc, я лучше возьму си, си++ или zig какой-нибудь, потому что они не мешают мне писать код. Если мне всё равно, есть ли в языке gc или нет, я возьму го для написания какой-то простой сетевой фигни, и возьму haskell для всего остального, в т.ч. для не самой простой сетевой фигни. Я не про прод говорю, если что, а про личные проекты.

Но тренд же сейчас наоборот в переходе с C/C++ на Rust, как раз за счет концепции управления памятью. А GC в Go наоборот записывают ему в минусы (в том виде, в котором он сейчас, из-за приостановки выполнения программы). Если нужен GC и "свобода" написания кода, то лучше выбрать Python или js. Я выбрал Go за то, что он компилируется в нативный код, статически типизированный, имеет свою концепцию интерфейсов вместо ООП и не требует ручного управления памятью (как С/С++), при этом синтаксически простой. После написания проекта на Go, и ряда последних статей на Хабре про Rust (где говорилось, что Go хуже), я решил еще раз на него обратить внимание (первый раз, давно, на нем писал драйвер и не рассматривал его как язык для прикладного ПО). И вот сейчас, после Go, я увидел что концепция та же, только не нужен GC, меньше кода писать, больше гибкости. Т.е. если сравнивать "в лоб", то код простого сервиса на Rust выглядит так же как на Go. Только для Go это "потолок", а для Rust "база". В итоге я пытаюсь услышать объективные доводы за/против перехода с Go на Rust. Пока кажется, что большинство разработчиков просто консервативны и противятся новому. (так же как в случае с Java и Kotlin)

Но тренд же сейчас наоборот в переходе с C/C++ на Rust, как раз за счет концепции управления памятью.

Может быть тренд и есть, но должны ли все следовать трендам? Управлять памятью безопасно можно и в си и в си++ во многих случах.

Если нужен GC и "свобода" написания кода, то лучше выбрать Python или js

Не люблю скриптовые языки, мне нравится статическая проверка типов, ничего не могу с собой поделать. Ещё хотелось бы спросить, почему лучше js или python, а не го, может у вас бенчмарки есть?

В итоге я пытаюсь услышать объективные доводы за/против перехода с Go на Rust.

Объективности здесь быть не может, у каждого из языков своя ниша. Давайте рассмотрим пример. Вам нужно написать какой-нибудь микросервис, суперпроизводительность кода в данном случае роли не играет, так как всё завязано на io (запросы к бд, сторонним сервисам и т.д.). Какой смысл писать на расте (если он мне не нравится), побеждая borrow checker, если можно взять условный го, получить удовлетворительный результат и не тратить при этом нервы на borrow checker? Да, то же самое можно сделать и на расте, но объективных причин его брать нет. Тут каждый выберет то, что ему больше по душе. Но опять же бывают случаи, где го неприменим, например в написании драйвера или приложения, которое должно в реальном времени работать. Тут уже появляется объективная причина выбора раст. Как видите ниши разные. Если для задачи подходит го, вы всё равно можете использовать раст, это уже дело вкуса, объективности тут нет.

Пока кажется, что большинство разработчиков просто консервативны и противятся новому. (так же как в случае с Java и Kotlin)

Свою позицию я высказал - мне важно, чтобы компилятор мне не сильно мешал, поэтому раст лично мне не нравится.

Не понимаю, в чем мешает вам компилятор rust?

Он используя лайфтаймами во время компиляции подсказывает какой объект/структура/ссылка жива или нет. Это же классно, не надо напрягать свой собственный мозг.

Данную работу все равно надо делать, так почему не делегировать это компилятору?

Данную работу все равно надо делать, так почему не делегировать это компилятору?

Потому что он слишком тупой, а логика проверок на "живость" слишком ограниченна и прямолинейна. И даже эта прямолинейная логика в нем реализована с ошибками.

Да, в чем то он прямолинейный, а где-то в этой логике есть ошибки. Но это не отменяет факта что rust снимает 99,9 % нагрузки с программиста.

Предпочту 99,9 % времени не париться и полагаться на компилятор, а на оставшиеся 0,1 % пользоваться unsafe, вместо того чтобы напрягаться все 100 % времени ради жалкого мизерного количества кода, когда компилятор rust действительно ошибается.

Кстати люди ошибаются чаще, особенно на больших объёмах кода

Вы спросили "почему" - вам ответили. Если в вашем случае он снимает 99.9% нагрузки с программиста, вы не работаете ни со сложными структурами данных типа многосвязных графов, ни с самоссылающимися структурами - ну, на здоровье. Но за всех говорить не стоит.

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

Но не будем спорить относительно необходимости использования многосвязных графов и самоссылающихся структур так уж часто как вы говорите.

Ваши аргументы похожи на то что нельзя использовать допустим формулы ускорения и вообще пользоваться евклидовым пространством. Ведь они в корне не верны в 0,1% случаев! Надо всегда пользоваться формулами из СТО, да и вообще мы живем в пространстве Римана.

Ваши аргументы похожи на то что нельзя использовать допустим формулы ускорения и вообще пользоваться евклидовым пространством. Ведь они в корне не верны в 0,1% случаев! Надо всегда пользоваться формулами из СТО, да и вообще мы живем в пространстве Римана.

А ваша настойчивость в определении того, что другим нужно или не нужно, удобно или неудобно, мне напоминает один известный анекдот. Еврейская семья. Мама, высунувшись из окна, зовет сына.
- Изя, срочно иди домой!
- Мама, я что, замерз?
- Нет, ты хочешь кушать!

Ну это уже классика, скоро юбилей будет: https://crates.io/crates/fake-static

Как решение - использовать miri - он вполне отлавливает этот случай.

Ага, классика. Которой сто лет в обед, но которую, несмотря на это, никак не починят.

Ну что есть то есть. Хотя вроде появилось направление решения этой проблемы. Но как минимум еще год будет открыт.

Однако, ведь такие трудно разрешимые баги есть во всех языках? Уж в C/C++ точно

Однако, ведь такие трудно разрешимые баги есть во всех языках? Уж в C/C++ точно

А можно пример чего-нибудь, что вы называете "трудно разрешимым багом в языке, уж в C/C++ точно"? В языке - это как?

Я не знаю, как вам это объяснить. Просто думать своим мозгом оказывается проще, чем подстраиваться под компилятор. И я не вижу лично для себя ни одной причины страдать и учиться под него подстраиваться так, чтобы это доставляло меньше боли. Мне проще взять старый добрый си, если нужно написать что-то низкоуровневое. В большинстве остальных случаев я вообще могу взять haskell и не париться. Если вам нравится раст - дело ваше, я же не говорю, что он плох, просто мне он не нравится.

думать своим мозгом оказывается проще, чем подстраиваться под компилятор
я вообще могу взять haskell и не париться

Редкий счастливый человек, которому не требуется подстраиваться под haskell, что тут ещё сказать :)

Я примерно также думаю о людях, которые легко пишут на расте :) В результате приходим к тому, что люди разные, и им нравятся разные инструменты.

А GC в Go наоборот записывают ему в минусы (в том виде, в котором он сейчас, из-за приостановки выполнения программы). Если нужен GC и "свобода" написания кода, то лучше выбрать Python или js.

Расскажу страшную тайну: во всех языках с GC он реализован через stop-the-world в том или ином виде. Это минус только если мы в лоб пытаемся применить их там, где обычно такие паузы неприемлимы.

имеет свою концепцию интерфейсов вместо ООП

Что значит "вместо" если это ООП и есть?

В итоге я пытаюсь услышать объективные доводы за/против перехода с Go на Rust

Как человек, который пишет на Rust в продакшен, могу сказать, почему на Rust не надо переходить:

  1. Очень долгая компиляция. Даже в не очень большом проекте у тебя будет порядка 500 сквозных зависимостей и их все нужно собирать, а сборка долгая. В CI/CD сборку приходится ждать иногда по 10 минут.

  2. В сложном проекте у тебя будет и async, и женерики, и макросы. Когда они все вместе встречаются - ты получаешь неочевидные причины ошибок, тормоза IDE, и нечитаемые сообщения об ошибках

  3. Некоторые части языка всё на стадии "дозревания". В частности - async в трейтах.

Что значит "вместо" если это ООП и есть?

Концепциями ООП являются инкапсуляция, полиморфизм и наследование. Как в Rust реализовано наследование?

Очень долгая компиляция. Даже в не очень большом проекте у тебя будет порядка 500 сквозных зависимостей и их все нужно собирать, а сборка долгая. В CI/CD сборку приходится ждать иногда по 10 минут

Собранные единожды зависимости разве не кэшируются?

Как в Rust реализовано наследование

Есть наследование поведения

Как в Rust реализовано наследование?

Это единственная вещь, которой напрямую в Rust нет, но наследование очень легко имитируется при помощи композиции.

Собранные единожды зависимости разве не кэшируются?

Кэшируются, но чтобы оно кэшировалось в CI/CD и между разными - придётся немного заморочиться.
Но это вполне решаемая проблема, да.

Но при обновлении зависимостей, компилятора, или изменении таргета - придётся снова ждать.

Очень долгая компиляция

Это у вас девопса нормального нет. Разделите на два-три докер образа и будет щастье. Первый образ для кэширования зависимостей, которые меняются нечасто, второй - для сборки, третий - для деплоя. Скорость будет такая же как и локально, основное время будет уходить на линковку, но и там можно подшаманить, если заменить дефолтный линковщик на "более лучший".

Разделите на два-три докер образа и будет щастье

Если не заморочиться с кэшированием, то компиляция будет в 2-3 раза дольше)

Первый образ для кэширования зависимостей, которые меняются нечасто, второй - для сборки, третий - для деплоя.

Может имел в виду слои?

то компиляция будет в 2-3 раза дольше

Или 20-30 ))

Может имел в виду слои?

Да, слои, конечно же, спасибо за ремарку

В Раст возникают сложности, когда пишешь код с ошибками (например, пытаешься шарить потоконебезопасную переменную между тредами). В си/++, действительно, в этом плане проще.

Писал на го, пишу на расте. Скажу, что на го писать - неприятно, многого не хватает в языке, писать на расте - сплошное удовольствие

В Раст возникают сложности, когда пишешь код с ошибками

Помнится, у меня были проблемы, когда я писал код без ошибок. Мне нужно было сделать кастомную структуру данных, и без unsafe сделать это было невозможно (во всяком случае мои попытки ни к чему не привели). Я пытался сделать таймер - передаёшь ему количество миллисекунд и callback, вызываешь в цикле метод с параметром delta, если прошло необходимое количество миллисекунд, он вызывает callback. Я так и не смог нормально это сделать, пришлось сделать так, чтобы этот метод возвращал bool, проверять результат на истинность и самому вызывать нужную функцию. Я уже точно не скажу, в чём конкретно были проблемы, так как это было давно, но возвращаться к языку желание отпалу полностью, потому что простые вещи, которые в си делаются на раз два, в расте делаются либо через unsafe, либо через боль.

А в чём проблема использовать немного unsafe при необходимости? В реализации на С ведь будет то же самое только неявно.

Ни в чём. Просто я не хочу долго думать, нужен тут unsafe или нет, это оказывается намного сложнее, чем просто сделать всю работу самому, а не полагаться на компилятор. Это главная причина, почему я не люблю раст - приходится думать не над задачей, а над тем, можно ли победить в данной ситуации borrow checker, и если можно, то как.

Звучит как как проблема нового инструмента. В других языках ведь тоже приходится постоянно принимать решения как правильно сделать. Просто когда это уже привычно, то на это тратится меньше времени.

Это главная причина, почему я не люблю раст - приходится думать не над задачей, а над тем, можно ли победить в данной ситуации borrow checker, и если можно, то как.

Ну а это скорее следствие относительной низкоуровневости языка. Если хочется максимально на предметной области сосредоточиться, то язык GC всегда будет лучше так как позволяет игнорировать разные "мелочи". Так-то в С(++) тоже приходится много в голове держать, просто компилятор не бьёт по рукам.

Звучит как как проблема нового инструмента. В других языках ведь тоже
приходится постоянно принимать решения как правильно сделать. Просто
когда это уже привычно, то на это тратится меньше времени.

Так и есть, просто я для себя решил, что решение этой проблемы лично мне не нужно, я просто откажусь от языка и не будет этой проблемы.

Ну а это скорее следствие относительной низкоуровневости языка. Если
хочется максимально на предметной области сосредоточиться, то язык GC
всегда будет лучше так как позволяет игнорировать разные "мелочи".
Так-то в С(++) тоже приходится много в голове держать, просто компилятор
не бьёт по рукам.

Собственно, поэтому я и пишу на си, а не на расте - компилятор не бьёт по рукам. Мне не нравится, как компилятор раста бьёт меня по рукам, я знаю, что то, что я делаю является безопасным с точки зрения использования памяти, но раст так не считает. Подстраиваться под компилятор раста у меня нет желания, поэтому я им и не пользуюсь. Но опять же, кому нравится, тот пусть пользуется, я никого не отговариваю. Мой тейк - мне не нравится раст, я не вижу причин для себя его осваивать, кому нравится, пусть пользуется.

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

Вот для таких случаев и нужен unsafe, да. Когда можно обосновать это "знаю" для будущих читателей кода (включая и себя самого) - чтоб они тоже знали и ничего не поломали.

Я понимаю, для чего нужен unsafe. Просто возникают ситуации, когда хочется использовать unsafe, но оказывается, можно и без него обойтись. Мне именно это и не нравится, что очень часто не понятно, а можно ли тут без unsafe обойтись. Жаль, конкретных примеров привести не могу, я слишком давно пробовал на rust писать, чтобы вспомнить, что конкретно у меня не получалось. В итоге я просто забросил этот проект.

Я пытался сделать таймер - передаёшь ему количество миллисекунд и callback, вызываешь в цикле метод с параметром delta, если прошло необходимое количество миллисекунд, он вызывает callback.

Честно, я не понимаю ни почему это должно быть сложно, ни почему нельзя сделать новый поток/асинхронную таску и вызвать там переданный коллбек после sleep.

А почему я должен создавать новый поток или асинхронную задачу под таймер в приложении, которое просто отрисовывает 3d сцену из yaml? Я не знаю, почему это сложно. Когда я передавал callback, который читает файлы с диска в структуры, borrow checker не давал упорно мне этого сделать, я плюнул и сделал таймер булевой функцией.

Могу предположить, что только концепция каналов.

Ошибка. Каналы в Rust есть и много: crossbeam::channel, tokio::sync::*, std::sync::*

GC в расчет не берем, т.к. в Rust он не нужен и в этом одно из его преимуществ

GC наоборот надо брать, тк добавление GC сразу избавляет от всех сложностей лайфтаймов и позволяет более-менее тривиально всякие self-referential структуры данных выражать.

А ещё в Rust нет такой концепции гринтредов, какая сделана в Go (не нужно нигде писать async-await, чтобы всё работало)

Что есть в Go, чего нет в Rust? Могу предположить, что только концепция каналов.

В расте есть каналы.

Так оберните typedef -ами и выиграете дважды:

Улчшете читаемость.

Наведете порядок в коде - за счет того что typedef обьявлен в одном месте в разы ускоряется компиляция и инкрементная компиляция.

Благо это или зло для разработки?

Да особо ничего не поменялось кроме пары специфичных кусков кода. Вот если бы обмазали стандартные контейнеры фп чейнами было бы намного веселее и приятнее.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории