В Go ввели generics и он потерял одно из своих основных преимуществ: легкую читаемость.
Накидал простой пример:
package main
import "fmt"
type MyObj[T int] struct{ a T }
type MyObj2[T MyObj[E], E int] struct{ b T }
type MyObj3[T MyObj2[A, E], A MyObj[E], E int] struct{ с T }
type MyObj4[T MyObj3[B, A, E], B MyObj2[A, E], A MyObj[E], E int] struct{ d T }
func main() {
my := MyObj[int]{a: 10}
my2 := MyObj2[MyObj[int], int]{b: my}
my3 := MyObj3[MyObj2[MyObj[int], int], MyObj[int], int]{с: my2}
my4 := MyObj4[MyObj3[MyObj2[MyObj[int], int], MyObj[int], int], MyObj2[MyObj[int], int], MyObj[int], int]{d: my3}
fmt.Println("Data: ", my, my2, my3, my4)
}
А в реальных проектах с кучей библиотек будут огромные строки указания параметров шаблона. Ни о какой легкой читаемости больше речь не идет, а ведь здесь мы всего лишь хотим сказать, что MyObj4.d содержит значение типа MyObj3.
Как же обходились без generics в Go 13 лет? Это не помешало выпустить отличные библиотеки и сделать хорошую стандартную библиотеку! При этом сохранялась лаконичность и легкость языка. А теперь...
У Rust и Go много общего: отсутствие классов, интерфейсы/трейты, принцип обработки ошибок и т.п. Если не брать во внимание GC, то кажется, что Rust - это улучшенная версия Go. Rust дает намного больше возможностей и гибкости, но берет "плату" в виде "владения" и "времени жизни" о которых нужно думать. На Rust можно так же начать писать в стиле Go, но все портят библиотеки. У Go, как мне кажется, отличные основные библиотеки (Gin, Fiber, GORM и т.п.) с отличной документацией из которой сразу понятно как использовать + за счет легкой читаемости языка можно быстро глянуть в IDE исходники нужной функции. А еще стандартная библиотека которая покрывает практически любые потребности. Интерфейс у всех фреймворков очень похож и можно быстро поменять одну библиотеку на другую.
В Rust не так. Тут принцип, что есть очень маленькая stdlib, а все дополнительные возможности должны писаться сообществом, поэтому появляются фреймворки типа Tokio. Минус такого подхода в том, что нет единого стандарта (как в Go) и каждый лепит API на свое усмотрение (получается этакий Python). Документация хуже чем у Go, часто просто сигнатуры методов и пара примеров, без описания, что и как. А еще макросы. Это все добавляет много сложности на начальном этапе выбора стека, но когда разберетесь с фреймворками и библиотеками, писать становится быстрее и меньше чем на Go. (хотя бы за счет другой обработки ошибок, из-за чего я и начал смотреть на Rust вместо Go)
Так вот про generics. Понадобилось мне в Rust использовать ORM так, то бы при запуске на локальном ПК использовался sqlite, а на сервере postgre. И это оказалось проблемой в библиотеках Rust. Что бы сделать например обобщённую функцию для sqlx нужно описать портянку из параметров шаблонов, притом это не очевидно и нужно долго выискивать, что где прописать. Но ведь в Go например есть GORM, который без всяких generics делает это. Получается, что generics усложняют код, но при этом они не нужны для реализации функционала.
Вот пример типа из Rust (с первого раза же понятно, что это за тип? сарказм ):
SelectStatement<FromClause<schema::users::table>,
diesel::query_builder::select_clause::SelectClause<diesel::expression::select_by::SelectBy<User, _>>,
diesel::query_builder::distinct_clause::NoDistinctClause,
diesel::query_builder::where_clause::WhereClause<diesel::expression::grouped::Grouped<diesel::expression::operators::Eq<schema::users::columns::username,
schema::users::columns::username>>>, diesel::query_builder::order_clause::NoOrderClause,
LimitOffsetClause<LimitClause<diesel::expression::bound::Bound<BigInt, i64>>,
NoOffsetClause>>
И это то, во что превратится Go с generics...
Раз теперь код в Go будет выглядеть как в Rust, то зачем продолжать использовать Go?
Получается только из-за работодателей, т.к. на рынке требуется много Go программистов и не требуются Rust.?
А что вы думаете по поводу generics (шаблонов). Благо это или зло для разработки?
Примечание: я не являюсь экспертом в Go и Rust и поэтому могу ошибаться в каких то моментах
Комментарии (84)
hardtop
24.11.2023 07:36-2Для разработки библиотек шаблоны удобны - писать меньше. А вот для конечных программистов - дело подготовки. Зачем нам ещё один Раст - не понятно. Уж лучше бы обработку ошибок сделали удобнее и лаконичнее, как в Zig, например.
vabka
24.11.2023 07:36+5А как в Zig сделано?
Просто я знаю три основных больших способа:
1. Коды ошибок, которые можно игнорировать (было сделано в Go изначально, хоть и не на столько плохо, как в си)
2. В функциональном стиле через Result или Either (сделано было в Rust и хочется видеть в Go)
3. Через исключения (чего не хочется теперь видеть нигде, но оно уже есть во многих языках)napolskih
24.11.2023 07:36-1А чем исключения плохи можете пояснить? если не говорить о перфомансе.
Я вот ни как не могу принять эти "новые концепции обработки ошибок". Все эти проверки кодов ответа кажутся откатом к темным временам.KanuTaH
24.11.2023 07:36-2Все эти проверки кодов ответа кажутся откатом к темным временам.
Вам не кажется, это он и есть.
MiyuHogosha
24.11.2023 07:36Обычно инх никто не проверят. Есть целая религия непроверки результата и errno (а это тот еще ктулху). Поэтому им нравится такой метод сообщения об ошибках.
vabka
24.11.2023 07:36+7Исключения плохи либо тем, что они многословные (как в случае с checked exception в java), либо тем что они являются неявным способом возврата значений.
И при этом сложно определить, когда исключение является обычной ошибкой, которую надо обработать, а когда - непредвиденной ситуацией, по которой нужно аварийно всё завершать.
По тому я считаю наиболее оптимальным решение с обработкой ошибок через паники (когда ситуация аварийная) и Result (когда ошибка предвиденная)napolskih
24.11.2023 07:36И при этом сложно определить, когда исключение является обычной ошибкой, которую надо обработать, а когда - непредвиденной ситуацией, по которой нужно аварийно всё завершать.
Это легко решается создание своего класса исключения (прикладного) и наследования от него всей нужной иерархии и их обработки. При этом код не засоряется всем этим мусором бесконечным.
Как практик с 20 летним стажем, к сожалению я не понял что вы хотели сказать. Громоздкость зависит от языка. Не вижу громоздкости например в Руби. По не явному возврату значения тоже не понятно. Почему объект исключения не может содержать ссылки или значения дополнительные? Ну и в нормальном языке (например Руби) тебе никто не мешает создать объект / метод у которого будет результат, в том числе говорящей о ошибке и возвращающей доп. инфу.
Как практик, в принудительном навязывании способа обработки ошибок через явную обработку возвращаемого значения каждой функции, я вижу очень вредный подход, который засорят код и заставляет писать и читать весь этот мусор.
Экономить нужно когнитивный ресурс человека (прежде всего того который будет годами читать этот код).
vabka
24.11.2023 07:36+3А ещё исключения очень дорогие и не позволяют выразить идею "эта функция ни при каких условиях не завершится с ошибкой".
А громоздкость обработки ошибок через Result решается через комбинаторы/оператор вопросительного знака/do-notation.
А как сделать свой доменный класс для ошибок и сделать так, чтобы ошибки из сторонних библиотек его наследовали?)
napolskih
24.11.2023 07:36А ещё исключения очень дорогие и не позволяют выразить идею "эта функция ни при каких условиях не завершится с ошибкой".
Видимо я не понял проблему. Перехвати все стандартные (не системные) исключения вот и все.
> А громоздкость обработки ошибок через Result решается через комбинаторы/оператор вопросительного знака/do-notation.
Покажите мне пожалуйста красивый, не мусорный код на го где этак концепция применяется и не нужно после каждого вызова явно проверять код ответа.> А как сделать свой доменный класс для ошибок и сделать так, чтобы ошибки из сторонних библиотек его наследовали?)
На руби это решается выносом базовых классов в базовую библиотеку (гем) или путем калбеков - одна библиотека выставляет калбек в котором ты в приложении можешь что-то проинициализировать (создать экземпляр нужного исключения) или на уровне конфигурации - передача прикладного класса исключения в приложении как опции для библиотеки.
vabka
24.11.2023 07:36+1Покажите мне пожалуйста красивый, не мусорный код на го где этак концепция применяется и не нужно после каждого вызова явно проверять код ответа.
А в 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 () () }
На руби это решается ...
Это на руби. Можно пример кода из других языков?
Это не выглядит как что-то дешёвое. Result по вычислительным ресурсам и памяти часто не дороже, чем сишные коды ошибок.
Видимо я не понял проблему. Перехвати все стандартные (не системные) исключения вот и все.
Как раз в этой необходимости перехватывать там, где это не нужно)
А ещё в том, что компилятор, имея информацию о том, что исключения никогда не будут выкинуты - может неплохо оптимизировать часть кода.
Free_ze
24.11.2023 07:36-1как сделать свой доменный класс для ошибок и сделать так, чтобы ошибки из сторонних библиотек его наследовали?
Перехватывать стронние исключения и заворачивать их в свои типы. Чтобы этот мусор не мешал читать алгоритм - библиотеку можно спрятать под прокси/фасад.
vabka
24.11.2023 07:36+1Получается мы берём технологию, которая создавалась для более лаконичной обработки ошибок (исключения) и полностью списываем её главное заявленное преимущество, обмазывая вызовы сторонних библиотек своими исключениями и фасадами?)
И при этом сохраняем все недостатки в виде нелинейности и дороговизны в рантайме.
Free_ze
24.11.2023 07:36Получается, что даже если хотеть странного, то можно найти способ сохранить код основного алгоритма в чистоте.
KanuTaH
24.11.2023 07:36не позволяют выразить идею "эта функция ни при каких условиях не завершится с ошибкой"
Если вы имеете в виду "эта функция ни при каких условиях не выбросит исключение" то отчего же, вполне себе позволяют. В C++ например для декларирования этого можно добавить к сигнатуре функции спецификатор
noexcept
.vabka
24.11.2023 07:36А во всех ли языках с исключениями имеется такая аннотация?
Да и обычно кажется, что функций, которые могут аварийно завершиться меньше, чем тех, которые аварийно не завершаются.
Ну и всё ещё не понятно, как отличить аварийную ситуацию (нарушение каких-то инвариантов, на которых вся система строится) от просто ошибки (нет доступа к файлу или не получается спарсить число).
Подход с Result и паниками как раз позволяет логично такое разделение показать.А ещё всё ещё не решается проблема с тем, что все возможные варианты исключений не перечисляются в сигнатуре функции.
KanuTaH
24.11.2023 07:36-1Подход с Result и паниками как раз позволяет логично такое разделение показать.
Паника - это жалкое подобие левой руки от раста, которую пытаются применять для эмуляции тех самых исключений, которые якобы "не нужны". Не просто так пытаются лепить всякие
catch_unwind
.А ещё всё ещё не решается проблема с тем, что все возможные варианты исключений не перечисляются в сигнатуре функции.
А эта проблема и вовсе существует только в вашей голове. Ладно, не интересно.
micronull
24.11.2023 07:36-4А чем исключения плохи можете пояснить?
В Go ошибки обрабатываются явным образом, что позволяет легко отслеживать ошибки в коде. Это делает код более понятным и предотвращает непредвиденное поведение.
В случае возникновения ошибки, Go требует, чтобы они были обработаны явно. Это позволяет программисту контролировать, как и когда каждая ошибка обрабатывается.
Нет неявного перехвата ошибок.
Возможность комбинирования ошибок
Подход обработки ошибок в Go прост в использовании и не внесет дополнительной сложности в код. Он также улучшает производительность, поскольку нет необходимости в неэффективной работе с исключениями и стеком вызовов.
В целом, подход к обработке ошибок в Go помогает создавать более чистый, управляемый и надежный код.
Free_ze
24.11.2023 07:36+4Разве исключения нельзя комбинировать? И если говорить про чистоту кода, то исключения тут как раз выигрывают: проверки ставятся только там, где это необходимо и нет нужны в бойлерплейте проброса ошибки выше.
Cerberuser
24.11.2023 07:36+5Уж лучше многословные шаблоны, чем
interface{}
на каждый чих. При том что на практике обе этих крайности почти всегда неправильные (и в Rust, когда ты сам пишешь код, а не отдаёшь его кодогенератору, можно и нужно вовремя вставитьBox<dyn Something>
, и в Go вполне можно многие вещи сделать более-менее типобезопасными).
ArchimeD
24.11.2023 07:36+7Назвать GORM основной библиотекой, да еще и отличной, да еще и понятной - это сильно. По мне так использовать ее можно только тогда, когда ты совершенно точно знаешь, зачем ты ее используешь, какие профиты из этого получишь, и четко представляешь, в какие проблемы это выльется по мере развития проекта.
itmind Автор
24.11.2023 07:36+2Какую ORM вы посоветуете для Go ?
vabka
24.11.2023 07:36+5Слышал мнение, что в Go просто не нужны ORM. Можно же просто сырой SQL использовать
itmind Автор
24.11.2023 07:36От CRM мне нужна поддержка подключения к разным СУБД через одно API и мапинг данных из строки БД на структуру. Сами запросы я предпочитаю писать на SQL.
vabka
24.11.2023 07:36+4Тогда есть sqlx - как раз ровно оно: общий API для управления транзакциями и подключения, но при этом запросы пишутся на SQL.
noRoman
24.11.2023 07:36+1Вот этого за глаза https://jmoiron.github.io/sqlx/
itmind Автор
24.11.2023 07:36-2На 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 был вроде один из самых быстрых.
noRoman
24.11.2023 07:36+1Пока у вас в меру простые запросы все хорошо. А потом будете бороться с GORM. SQL запрос вроде как написан и оттестирован, но уложить его в GORM... бывает вызвает проблему. Может что-то и изменилось за 2 года, но я зарекся с ним больше не связываться
itmind Автор
24.11.2023 07:36У GORM есть raw SQL запросы https://gorm.io/docs/sql_builder.html
noRoman
24.11.2023 07:36+1Я могу сразу написать нужные модели и репозитории и не тянуть GORM). Пару заготовок есть и времени на создание много времени у меня не занимает. Каждому свое. Вам удобно - пользуйтесь на здоровье.
xxxphilinxxx
24.11.2023 07:36+9Не разделяю опасения.
В 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 все так же будет иметь намного более низкий порог входа и простоту поддержки легаси, все так же будет управлять сбором мусора, все так же будет хорош легковесными горутинами и т.д.
itmind Автор
24.11.2023 07:36-1Согласен с вашими высказываниями. Насчет языков мне кажется у Go и у Rust одинаковые концепции, они близки по подходам к разработке (через интерфейсы/трейты, модули). Rust решил проблему утечек памяти и рантайм ошибок, Go обеспечил быстрое обучение + быстрая разработка мелких программ. Оба языка обеспечивает небольшое использование памяти программами и отличное быстродействие. Но лично мне не нравится в Go после вызова каждой функции (которая возвращает ошибку) писать обработку ошибки.
vabka
24.11.2023 07:36+1Ну изначально Rust зарождался как второй Go с гринтредами и сборкой мусора, но потом ушёл в сторону безопасности во время компиляции и zero cost.
micronull
24.11.2023 07:36Но лично мне не нравится в Go после вызова каждой функции (которая возвращает ошибку) писать обработку ошибки.
Попробуйте приучить себя расширять контекст ошибки при прокидывании.
Например:
if err := some.Sum(a, b); err != nil { return fmt.Errorf("couldn't summarize %d and %d: %w", a, b, err) }
Вам это здорово поможет при расследовании инцидентов и отладке.
vabka
24.11.2023 07:36+6Про то что это неправильно называть шаблонами уже сказали.
В остальном - на мой взгляд женерики - это точно хорошо, а не плохо.
Без них просто нельзя было бы сохраняя строгую статическую типизацию описывать обобщённые структуры, как это было в примере кода на Rust.
> Вот пример типа из Rust (с первого раза же понятно, что это за тип? сарказм ):Ага, было бы гораздо понятнее, если бы был
interface {}
или какой-нибудьAny
илиObject
, обмазанный рефлексией в рантайме.Там где обобщения не нужны - их можно просто не использовать.
napolskih
24.11.2023 07:36-3Давайте популяризировать и развивать язык Crystal!
https://crystal-lang.org/itmind Автор
24.11.2023 07:36На нем можно писать под web (backend+frontend, wasm), Android, iOS, UI приложения для Windows, Linux, MacOS ?
napolskih
24.11.2023 07:36Backend можно и даже очень хорошо. на счет UI не уверен. Не все сразу. Развивайте, двигайте. Я вот доначу регулярно уже много лет.
https://github.com/hugopl/gtk4.cr
vabka
24.11.2023 07:36+4А в чём профит? Даже разработчики языка не могут у себя на главной показать своё УТП.
Go - Очень простой, поддерживается всеми большими корпорациями, заточен под concurrency из коробки.
Rust - Производительный, надёжный, безопасный
Crystal - Руби со статической типизацией и гринтредами?napolskih
24.11.2023 07:36-3На главной же написано язык для людей. Тем не менее и для компьютера то есть быстрый.
includedlibrary
24.11.2023 07:36+2Язык для людей - очень пространное описание. Синтаксис COBOL сделали многословным тоже под этим предлогом. Я не к тому, что мне crystal не нравится, а к тому что "для людей" не может быть конкретной целью, потому что разным людям разные вещи нравятся
lanseg
24.11.2023 07:36+4Go без дженериков использовать было очень неудобно, сейчас он, хотя бы, стал чуть удобнее. Ещё бы дженерики были полноценные, чтобы можно было у "метода" использовать не только те типы, которые объявлены у родительского struct/interface
itmind Автор
24.11.2023 07:36-6Вы считаете, что в примере из статьи дженерики улучшили код? Этот код легко прочитать и понять?
includedlibrary
24.11.2023 07:36+13Вы считаете, что ваш пример является хорошим? Приведите пример, где использование дженериков оправдано, например в контейнерах, покажите, что с ними код стал хуже, и покажите, что без них он становится лучше. А то я могу доказать, что оператор сложения делает код хуже:
int main(void) { // умножим пять на пять int result = 5 + 5 + 5 + 5 + 5; // ох, уж этот плюсик }
А теперь сделаем без сложения
int main(void) { // умножим пять на пять int result = 5 * 5; // Видите, плюсик - зло }
Вот таким нехитрым образом можно доказать, что оператор сложения - зло
GoodOldDude
24.11.2023 07:36Благо это или зло для разработки?
Ни то, ни другое, это просто инструмент обобщенного программирования. А вот как этим уже будут пользоваться – другой вопрос. В целом писать трудночитаемый (и труднопонимаемый в том числе) код можно и без дженериков, шаблонов и макросов.
stvoid
24.11.2023 07:36По факту использую дженерики в 2% кода максимум и то уже после рефакторинга, когда становистя понятно что какие-то функции можно переписать на дженерики просто для уменьшения кода.
Тоже опасался что они будут везде, но нет, это не так.
Free_ze
24.11.2023 07:36+4Вот пример типа из 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>>
Anarchist
24.11.2023 07:36+1Продолжая логику: а вот раньше на C вообще без классов писали, а теперь нагромоздили всего.
Кстати, классы в Rust есть. Просто они type classes, что (на мой вкус) является очень правильной реализацией ООП.
Rust вообще получился в том числе очень неплохой проработкой родовых травм других языков - наследования, нуллов, исключений.
Go изначально строился с расчетом на джунов и простые приложения, может, и правда не стоит туда вводить парадигмы из серьёзных языков. В Perl тоже в своё время напихали всего. Лучше уж сразу переехать на что-то более подходящее, у очень костыльно выходит, если язык не по назначению использовать. Либо перерабатывать язык от корней.
itmind Автор
24.11.2023 07:36Логика статьи не в том, что дженирики плохо, а в том, что если в Go превращается в Rust, то почему бы сразу не писать на Rust?
includedlibrary
24.11.2023 07:36В расте тоже есть дженерики, почему бы не писать на c++, haskell, etc. ведь в них тоже есть дженерики, а значит раст превращается в них, зачем на нём писать? Раст от го не только наличием дженериков отличается, поэтому утверждение, что го превращается в раст некорректно.
itmind Автор
24.11.2023 07:36Мне пока видится, что Go это подмножество Rust. У того же С++ другая концепция (ООП, классы, ручной контроль за памятью). Что есть в Go, чего нет в Rust? Могу предположить, что только концепция каналов. (GC в расчет не берем, т.к. в Rust он не нужен и в этом одно из его преимуществ). Никто из комментаторов ни приводит ни одного довода за или против Go/Rust, все суждения не объективны. Я пересмотрел кучу статей "сравнений" этих двух языков и везде только общие слова, как будто написанные GPT, никакой конкретики, примеров.
includedlibrary
24.11.2023 07:36Что есть в Go, чего нет в Rust?
GC. Да, как вы сказали, в расте он не нужен, но из-за этого возникают сложности с написанием кода. Собственно, поэтому я иногда использую го, но никогда не захочу использовать раст. Если мне нужен язык без gc, я лучше возьму си, си++ или zig какой-нибудь, потому что они не мешают мне писать код. Если мне всё равно, есть ли в языке gc или нет, я возьму го для написания какой-то простой сетевой фигни, и возьму haskell для всего остального, в т.ч. для не самой простой сетевой фигни. Я не про прод говорю, если что, а про личные проекты.
itmind Автор
24.11.2023 07:36Но тренд же сейчас наоборот в переходе с C/C++ на Rust, как раз за счет концепции управления памятью. А GC в Go наоборот записывают ему в минусы (в том виде, в котором он сейчас, из-за приостановки выполнения программы). Если нужен GC и "свобода" написания кода, то лучше выбрать Python или js. Я выбрал Go за то, что он компилируется в нативный код, статически типизированный, имеет свою концепцию интерфейсов вместо ООП и не требует ручного управления памятью (как С/С++), при этом синтаксически простой. После написания проекта на Go, и ряда последних статей на Хабре про Rust (где говорилось, что Go хуже), я решил еще раз на него обратить внимание (первый раз, давно, на нем писал драйвер и не рассматривал его как язык для прикладного ПО). И вот сейчас, после Go, я увидел что концепция та же, только не нужен GC, меньше кода писать, больше гибкости. Т.е. если сравнивать "в лоб", то код простого сервиса на Rust выглядит так же как на Go. Только для Go это "потолок", а для Rust "база". В итоге я пытаюсь услышать объективные доводы за/против перехода с Go на Rust. Пока кажется, что большинство разработчиков просто консервативны и противятся новому. (так же как в случае с Java и Kotlin)
includedlibrary
24.11.2023 07:36Но тренд же сейчас наоборот в переходе с C/C++ на Rust, как раз за счет концепции управления памятью.
Может быть тренд и есть, но должны ли все следовать трендам? Управлять памятью безопасно можно и в си и в си++ во многих случах.
Если нужен GC и "свобода" написания кода, то лучше выбрать Python или js
Не люблю скриптовые языки, мне нравится статическая проверка типов, ничего не могу с собой поделать. Ещё хотелось бы спросить, почему лучше js или python, а не го, может у вас бенчмарки есть?
В итоге я пытаюсь услышать объективные доводы за/против перехода с Go на Rust.
Объективности здесь быть не может, у каждого из языков своя ниша. Давайте рассмотрим пример. Вам нужно написать какой-нибудь микросервис, суперпроизводительность кода в данном случае роли не играет, так как всё завязано на io (запросы к бд, сторонним сервисам и т.д.). Какой смысл писать на расте (если он мне не нравится), побеждая borrow checker, если можно взять условный го, получить удовлетворительный результат и не тратить при этом нервы на borrow checker? Да, то же самое можно сделать и на расте, но объективных причин его брать нет. Тут каждый выберет то, что ему больше по душе. Но опять же бывают случаи, где го неприменим, например в написании драйвера или приложения, которое должно в реальном времени работать. Тут уже появляется объективная причина выбора раст. Как видите ниши разные. Если для задачи подходит го, вы всё равно можете использовать раст, это уже дело вкуса, объективности тут нет.
Пока кажется, что большинство разработчиков просто консервативны и противятся новому. (так же как в случае с Java и Kotlin)
Свою позицию я высказал - мне важно, чтобы компилятор мне не сильно мешал, поэтому раст лично мне не нравится.
JustForFun88
24.11.2023 07:36+1Не понимаю, в чем мешает вам компилятор rust?
Он используя лайфтаймами во время компиляции подсказывает какой объект/структура/ссылка жива или нет. Это же классно, не надо напрягать свой собственный мозг.
Данную работу все равно надо делать, так почему не делегировать это компилятору?
KanuTaH
24.11.2023 07:36-2Данную работу все равно надо делать, так почему не делегировать это компилятору?
Потому что он слишком тупой, а логика проверок на "живость" слишком ограниченна и прямолинейна. И даже эта прямолинейная логика в нем реализована с ошибками.
JustForFun88
24.11.2023 07:36+1Да, в чем то он прямолинейный, а где-то в этой логике есть ошибки. Но это не отменяет факта что rust снимает 99,9 % нагрузки с программиста.
Предпочту 99,9 % времени не париться и полагаться на компилятор, а на оставшиеся 0,1 % пользоваться unsafe, вместо того чтобы напрягаться все 100 % времени ради жалкого мизерного количества кода, когда компилятор rust действительно ошибается.
Кстати люди ошибаются чаще, особенно на больших объёмах кода
KanuTaH
24.11.2023 07:36-1Вы спросили "почему" - вам ответили. Если в вашем случае он снимает 99.9% нагрузки с программиста, вы не работаете ни со сложными структурами данных типа многосвязных графов, ни с самоссылающимися структурами - ну, на здоровье. Но за всех говорить не стоит.
JustForFun88
24.11.2023 07:36+1Работаю, и он там это все тоже снимает. Небезопасный код приходиться использовать только точечно.
Но не будем спорить относительно необходимости использования многосвязных графов и самоссылающихся структур так уж часто как вы говорите.
Ваши аргументы похожи на то что нельзя использовать допустим формулы ускорения и вообще пользоваться евклидовым пространством. Ведь они в корне не верны в 0,1% случаев! Надо всегда пользоваться формулами из СТО, да и вообще мы живем в пространстве Римана.
KanuTaH
24.11.2023 07:36-1Ваши аргументы похожи на то что нельзя использовать допустим формулы ускорения и вообще пользоваться евклидовым пространством. Ведь они в корне не верны в 0,1% случаев! Надо всегда пользоваться формулами из СТО, да и вообще мы живем в пространстве Римана.
А ваша настойчивость в определении того, что другим нужно или не нужно, удобно или неудобно, мне напоминает один известный анекдот. Еврейская семья. Мама, высунувшись из окна, зовет сына.
- Изя, срочно иди домой!
- Мама, я что, замерз?
- Нет, ты хочешь кушать!
vabka
24.11.2023 07:36Ну это уже классика, скоро юбилей будет: https://crates.io/crates/fake-static
Как решение - использовать miri - он вполне отлавливает этот случай.
KanuTaH
24.11.2023 07:36Ага, классика. Которой сто лет в обед, но которую, несмотря на это, никак не починят.
JustForFun88
24.11.2023 07:36Ну что есть то есть. Хотя вроде появилось направление решения этой проблемы. Но как минимум еще год будет открыт.
Однако, ведь такие трудно разрешимые баги есть во всех языках? Уж в C/C++ точно
KanuTaH
24.11.2023 07:36Однако, ведь такие трудно разрешимые баги есть во всех языках? Уж в C/C++ точно
А можно пример чего-нибудь, что вы называете "трудно разрешимым багом в языке, уж в C/C++ точно"? В языке - это как?
includedlibrary
24.11.2023 07:36Я не знаю, как вам это объяснить. Просто думать своим мозгом оказывается проще, чем подстраиваться под компилятор. И я не вижу лично для себя ни одной причины страдать и учиться под него подстраиваться так, чтобы это доставляло меньше боли. Мне проще взять старый добрый си, если нужно написать что-то низкоуровневое. В большинстве остальных случаев я вообще могу взять haskell и не париться. Если вам нравится раст - дело ваше, я же не говорю, что он плох, просто мне он не нравится.
Cerberuser
24.11.2023 07:36+1думать своим мозгом оказывается проще, чем подстраиваться под компилятор
я вообще могу взять haskell и не паритьсяРедкий счастливый человек, которому не требуется подстраиваться под haskell, что тут ещё сказать :)
includedlibrary
24.11.2023 07:36+1Я примерно также думаю о людях, которые легко пишут на расте :) В результате приходим к тому, что люди разные, и им нравятся разные инструменты.
vabka
24.11.2023 07:36А GC в Go наоборот записывают ему в минусы (в том виде, в котором он сейчас, из-за приостановки выполнения программы). Если нужен GC и "свобода" написания кода, то лучше выбрать Python или js.
Расскажу страшную тайну: во всех языках с GC он реализован через stop-the-world в том или ином виде. Это минус только если мы в лоб пытаемся применить их там, где обычно такие паузы неприемлимы.
имеет свою концепцию интерфейсов вместо ООП
Что значит "вместо" если это ООП и есть?
В итоге я пытаюсь услышать объективные доводы за/против перехода с Go на Rust
Как человек, который пишет на Rust в продакшен, могу сказать, почему на Rust не надо переходить:
Очень долгая компиляция. Даже в не очень большом проекте у тебя будет порядка 500 сквозных зависимостей и их все нужно собирать, а сборка долгая. В CI/CD сборку приходится ждать иногда по 10 минут.
В сложном проекте у тебя будет и async, и женерики, и макросы. Когда они все вместе встречаются - ты получаешь неочевидные причины ошибок, тормоза IDE, и нечитаемые сообщения об ошибках
Некоторые части языка всё на стадии "дозревания". В частности - async в трейтах.
itmind Автор
24.11.2023 07:36Что значит "вместо" если это ООП и есть?
Концепциями ООП являются инкапсуляция, полиморфизм и наследование. Как в Rust реализовано наследование?
Очень долгая компиляция. Даже в не очень большом проекте у тебя будет порядка 500 сквозных зависимостей и их все нужно собирать, а сборка долгая. В CI/CD сборку приходится ждать иногда по 10 минут
Собранные единожды зависимости разве не кэшируются?
vabka
24.11.2023 07:36+1Могу предположить, что только концепция каналов.
Ошибка. Каналы в Rust есть и много: crossbeam::channel, tokio::sync::*, std::sync::*
GC в расчет не берем, т.к. в Rust он не нужен и в этом одно из его преимуществ
GC наоборот надо брать, тк добавление GC сразу избавляет от всех сложностей лайфтаймов и позволяет более-менее тривиально всякие self-referential структуры данных выражать.
А ещё в Rust нет такой концепции гринтредов, какая сделана в Go (не нужно нигде писать async-await, чтобы всё работало)
DarkEld3r
24.11.2023 07:36Что есть в Go, чего нет в Rust? Могу предположить, что только концепция каналов.
В расте есть каналы.
bekhruz
В плане шаблоны? Немного не понял. Может имелись в виду дженерики?
Akuma
Я вот тоже подозреваю, что автору не нравятся дженерики.
Только вот они на любом языке будут довольно многословные, просто потому что это так работает.
Leopotam
Обобщения они зовутся по-русски, а так да, generics.
itmind Автор
Шаблонами это называется в С++. С тех пор и в других языках называю это шаблонами.
KanuTaH
Да не называется "это" шаблонами в C++. Идея шаблонов в C++ основана на duck typing через инстанцирование шаблона "по месту", в то время как идея дженериков основана на "низведении" передаваемых типов к некоему общему знаменателю через type erasure в том или ином виде. Разница принципиальная. Например, в C++ вы можете сделать так:
в то время, как ни в Go, ни в Rust ничего подобного сделать нельзя, нужно в дженерике явно указывать ограничения для типов:
shai_hulud
Дженерики не всегда сделаны через type erasure.
KanuTaH
Я написал "в том или ином виде", переданный тип внутри дженерика по сути раскладывается на набор неких трейтов (или, если угодно, интерфейсов), с которыми можно делать то, что делается с этим типом внутри дженерика, и этот набор трейтов/интерфейсов нужно прописывать руками.
bBars
Насколько мне известно, в го применяются разные подходы в случае с функциями и в случае с типами. Для функций предусмотрен механизм «шаблонных функций», типа как в плюсах: компилятор находит все варианты использования и генерирует несколько вариантов для всех задействованных — потому они не дают оверхеда. А вот для типов на этапе компиляции проводятся лишь поверхностные проверки совместимости, а в рантайме начинается цирк с приведениями, что бьёт по производительности.
Ну и нет нужды явно указывать тип в момент вызова. Если компилятор может догадаться, то он это сделает. Да, ограничения накладывать нужно при определении типа или функции — для выявления допустимых операций над типом. По сути это упрощение поиска ошибок типа «нет подходящего operator*», но только заложенное в сам дженерик, а не в место вызова.