Обновить

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

Мантра гошников: «В Go этого нет», «В Go это реализовано иначе»

Абсолютно правильная статья

Особенно когда автор и, судя по всему, большинство комментаторов не умеют в Go;) ИМХО: статья о том, что прежде чем браться за работу, нужно изучать матчасть. Я, например, не умею Rust, мне не понятен его синтаксис. Хотя приходилось пару раз править код на Rust и Elixir. Но я не пишу, что это "убогий" язык;)

Давайте поясните. "Уметь в го" это как? С чего вы взяли что я "не могу"?

У вас есть какие-то контрпримеры к статье?

Начнем с того, что автор подменяет понятия. Ребята, вы не "пользователи" библиотеки, вы программисты. ИМХО: Да, надо просматривать чужие пакеты/библиотеки, что бы в продакшн не было сюрпризов. Что касается enum, передача int вместо константы, это ошибка программиста, а не пользователя. Далее не понятно утверждение автора о том, что в Go нет типизированных ошибок. Судя по коду проверки, автор даже не пытался разобраться. Во многих случаях автор статьи указывает на проблемы с копипастой, а это уже человеческий фактор, а не ошибки ЯП. Так же автор возмущается тем, что в Go нельзя использовать синтаксический сахар и приходится проверять переменную ошибки, в тоже время пишет, что для одного канала необходимо возвращать две переменных. Вся статья, по сути, основана на привычках автора, а не особенностях языка. А, как говорится, на вкус и цвет, фломастеры разные. Есть в Go моменты, которые и мне не нравятся. Например, серилизация структур из-за выравнивания в памяти. Но утверждать, что Go не подходит для больших проектов... Вы все дружно пользуетесь докерами, кубиками и т.д. Что-то пока не видел потенциальной замены на Rust

Автор сам придумал философию Go и сам же доказывает ее не состоятельность


У меня есть замечательный пример:


Сделали приложение-монолит на Go

Потом решили заняться оптимизацией одного из модулей, переписали на C++ 

Потратили: 280 часов двух сильных мидлов чтобы получить 4% скорости и -30% расход RAM на одном из 8 модулей

….только вот на весь монолит от нуля до запуска потратити 600 часов

Список задач довольно ограничен, где целесообразно будет вкидывать деньги в 4% скорости и экономию RAM.

И думаю (имхо) в этом философия Go, несмотря на все недостатки и нюансы, которых к слову очень много, – Go позволяет на коленке создавать действительно очень быстрые приложения любого размера.

ПС: Очень грубо прикину, что если бы из примера писали монолит изначально на Rust потратили бы раз в 5 больше времени, но скорость была бы не 5x, а дай бог вытащили в пределах 10%

Как вообще связаны C++ и Rust? У вас есть опыт с C++, делайте выводы про C++.

ПС: Очень грубо прикину, что если бы из примера писали монолит изначально на Rust потратили бы раз в 5 больше времени, но скорость была бы не 5x, а дай бог вытащили в пределах 10%

Цифры с потолка что называется. Откуда вообще разница в 5 раз между разработкой на Go и на Rust. Если программисты в Rust разбираются, то время разработки было бы примерно такое же (с учётом того, что некоторые баги не возникали бы вообще, благодаря особенностям Rust).

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

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

Да, надо просматривать чужие пакеты/библиотеки, что бы в продакшн не было сюрпризов.

Нет, не надо, если у интерфейса библиотеки есть нормальная документация, где описано её поведение. (Пример: стандартная библиотека Rust).

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

Вы про что? Где я пишу что нет типизированных ошибок? Я говорю что в Go передают ошибки в виде интерфейса error. И даже если ошибки типизированные, то нам надо тайпкастить (я в статье привёл пример с err.(FileIsNotEmpty)).

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

Но разные языки в разной степени подвержены человеческому фактору. Это вот хардкорные фанаты Си тоже любят говорить, что use-after-free и dangling-pointer это ошибка программиста, а значит человеческий фактор. По-моему я продемонстрировал, что в расте с меньшей вероятностью копипаста к багам приводит.

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

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

Да, я считаю, что программист должен напрягаться больше конечного пользователя. Если в функции аргументом указан тип A, то и переменную необходимо передавать этого типа, а не другого. Если в документации нет, достаточно посмотреть на константы и объявленные переменные. Что касается "нормальной" документации, то это ошибки разработчика библиотеки, а не какого-то конкретного ЯП. Во поводу "тайпкастить" (ну и словечко) ошибки: посмотрите например на функционал errors.Is и errors.As. Видно, что вы даже не пытались разобраться в теме. Ну и на последок:

fn load_block_constants(&mut self, hash_header: &[u8; 72], target: &[u64; 4]) {
        let mut hash_header_gpu = self._module.get_global::<[u8; 72]>(&CString::new("hash_header").unwrap())
            .map_err(|e| {
                error!("Failed to get global 'hash_header': {}", e);
                Error::from(e.to_string())
            }).unwrap();
        hash_header_gpu.copy_from(hash_header).map_err(|e| {
            error!("Failed to copy hash_header: {}", e);
            Error::from(e.to_string())
        }).unwrap();

        let mut target_gpu = self._module.get_global::<[u64; 4]>(&CString::new("target").unwrap())
            .map_err(|e| {
                error!("Failed to get global 'target': {}", e);
                Error::from(e.to_string())
            }).unwrap();
        target_gpu.copy_from(target).map_err(|e| {
            error!("Failed to copy target: {}", e);
            Error::from(e.to_string())
        }).unwrap();
    }

Как тут кастуются ошибки?

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

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

Если в функции аргументом указан тип A, то и переменную необходимо передавать этого типа, а не другого.

Это вы к чему?

Что касается "нормальной" документации, то это ошибки разработчика библиотеки, а не какого-то конкретного ЯП.

Я не имел в виду что это проблема языка. Это проблема культуры разработчиков языка. В Расте программисты вполне могли бы везде позвращать трейт std::error::Error, вместо конкретных ошибок, язык не запрещает. Но так как культура у языка другая, люди так не делают. Точно так же с документацией: стандартная библиотека раста подаёт образцовый пример документации, в отличии от стандартной библиотеки Go.

errors.Is и errors.As

Я про них знаю, но они добавляют кучу своих проблем. (Стоило конечно в статье написать про них, но я прохлопал).

Во первых, As принимает any, то есть пробема с тем, что я передал MyError вместо *MyError остаётся. Узнаю об ошибке в рантайме, js-стайл. Is хотя бы принимает error, но не буду же я делать Is, а потом приведение типов только ради compile-type проверок.

Во вторых, они анализируют всё дерево ошибок. Что если мне не нужно анализировать вложенные ошибки? Это может привести к ложным срабатываниям -- я хочу проверить определённую ошибку только на самом верхнем уровне, а она есть на более низком уровне, и я получу true. Почему нельзя было добавить вариант функции, проверяющий только верхний уровень - загадка.

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

По итогу в стандартной библиотеке Go удобной обработки ошибок всё ещё нет.

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

Теперь нужно пройти следующего босса :-). В Rust алгебраические типы данных (ADT). Это даёт очень мощные возможности.

не уверен, что это сделало бы go лучше

Каналы в Go сделаны неудобно. Начнём с того, что посылающая и принимающая сторона выражена одним и тем же типом

Есть <-chan и chan<-

Не знал. На этот счёт есть офф документация? Просто в tour of go этого точно нет

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

Кстати, в какой версии языка это добавили?

В v1.0.0 было https://github.com/golang/go/blob/go1/doc/go_spec.html#L1256

Не понимаю за что мне заминусили комментарий

Наверное, не понравилось, что опираетесь, по сути, на ознакомительный тур.
В tourofrust каналы вообще не упоминаются, вроде.

Ну, я не упираюсь в tour of go, просто это материал который в первую очередь будут изучать новички и в нём даётся плохой совет на счёт каналов (зачем писать "не закрывайте на читающей стороне", когда можно написать "используйте <-chan")

На самом деле это отличный совет. Кто канал создал, тот его и закрывает. Чем раньше новичок это будет знать, тем лучше

Но всё таки лучше когда на уровне компилятора это отслеживается (как с <-chan и chan<-), тогда новичок такой ошибки вообще не сможет совершить

Первый недостаток Go -- это отсутствие перечислений (enum).

Поэтому объявляют специализированный тип и набор констант через iota и не парятся. Или используют кодогенератор и не парятся. Или используют пакет enum и его дженерики и снова не парятся.

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

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

А ситуацию когда есть набор вариантов из которых нужно выбрать ровно один на Go можно оформить массой способов - в Go функции are first class citizens.

Почему эта задача плохо решается с помощью Go. В Go для этой задачи придётся использовать обычный int

Не "придётся использовать", а "я не могу придумать ничего кроме".

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

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

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

Некоторе системы требуют писать без выделения и освобождения памяти, но это к слову. Сама же идея интересная - кем позиционируется? Где? Когда? Зачем? Я вижу что про языки программирования несут чушь на уровне торгашей базарных и пропагандистов продажных, но не понимаю кто заказывает эту музыку.

Писать высоконагруженные (то есть с фиксированным временем реакции, что-ли?) системы на Go точно можно, но что для Go, что для Rust точно найдутся такие, что их проще писать на другом языке. Elixir не даст соврать... И чё?

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

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

Таким образом, Go подталкивает нас к использованию интерфейсов

Ну вот... понимают если захотят.

Из-за того, что кортежи -- это не настоящий тип, мы не можем их использовать в генериках:

Не "не можем", а "не должны". Извращаться можно где угодно, но зачем? Кортеж - собранные вместе несколько значений. Ничего не напоминает? А их возвращение из функции - синтаксический сахар, полезность которого доказана обработкой ошибок. Как и := оператора...

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

Да, у Go есть история.

Это полный трэш и по сути игнорирование строгой типизации языка.

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

В подходе, который в Go выбран для обработки ошибок, мне нравятся две вещи: передача ошибок через возвращаемое значение функций (по сравнению с исключениями в C++, Java) и возврат ошибки как отдельного значения (по сравнению с использованием "особых значений" как в C).

Ну не надо такое писать, дурно пахнет. Да, все так пишут, запах только усиливается. В Go перехватывается panic...

Первая проблема с таким подходом в том, что по сигнатуре функции нельзя понять, какие ошибки внутри неё могут возникнуть.

Это уже зашкаливает. Используйте другой тип и всё. Если Rust создаёт выученную беспомощность заставляющую использовать только то, что есть в стандартной библиотеке, без изменения, пусть и не всех (на меня вот не подействовало), то это проблема Rust.

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

То же самое опять. "Обычно" не значит "всегда".

И у вас появляется переменная, существующая на протяжении всей функции, которую можно случайно использовать.

Давно решено. См. := оператор.

Сложно не упомянуть о синтаксическом сахаре в виде ?.

Спасибо что упомянули. Это как раз то, что было сознательно отвергеуто в Go. Упрощает и без того тривиальное ценой усложнения и так нетривиального.

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

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

Непонятно, что мешало за столько лет существования языка добавить банальное:

Да, в Go есть история и изначально в конструкциях языка была некоторая магия. Что лучше, append или .Push - не очевидно, это разные вещи потому, что append добавляет элемент не в массив, а в slice.

Видите баг? Мы создали массив, в котором уже есть 10 элементов.

Не вижу. Создали массив из 10 элементов. Молодцы. Не хватает фантазии сообразить когда это нужно?

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

Я бы первым делом заподозрил array literal.

Начнём с того, что посылающая и принимающая сторона выражена одним и тем же типом:

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

В Go присутствует такая эзотерическая конструкция, как теги на полях структур.

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

Тогда после вызова специальной команды go generate будет произведена генерация кода. Это очень хрупкий подход, потому что корректность написанной вами команды не проверяется никем.

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

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

И не должны мочь. По определению родительский модуль использует дочерний.

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

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

Существуют //go:build тэги, возможность явно перечислить файлы в коммандах run и build, ссылки в файловой системе, файл go.work. Вместе вот это всё позволяет раскладывать файлы по папкам крайне кучерявым образом, но я не могу придумать когда такое действительно нужно. Может быть для генераторов кода? Может быть тогда эта избыточность понадобится. А может просто избыточность введена для того, чтобы было проще обмениваться файлами по почте? Не знаю...

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

Ещё раз, мало ли что и из каких соображений врут говорят. Возьмём два slice (среза?) от одного массива и начнём их изменять - ничего от простоты Go не останется. Напишем горутины и сравним работу кода на десктопе и в Web Assembly - результат тот же. И далее везде.

Я бы мог согласиться с тем, что Go прост, если бы из контекста следовало, что он идеологически прост. Это не означает ни низкого порога входа, ни удобства для не понимающих сути языка, что Вы и описали. Как пример - Scheme и того идеологически проще... И ещё более охотно я бы согласился с тем, что Go идеологически прост и практичен.

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

Негибкости и неудобства я пока в Go не почуствовал. Наоборот, с Go приятней работать чем с чем-либо ещё. Следующий за Go вариант - сладкая парочка JavaScript + Dart. Rust мне тоже нравится, наличие архитектурных ошибок странностей не особо мешает, но пользовать его можно только если время ожидания окончания компиляции хорошо оплачивается.

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

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

через iota и не парятся

пишут склонный к багам код и "не парятся", окей

А если ни то ни другое не нужно? Это, кстати, философски значимо - компилятор для программиста или программист для компилятора? Или даже так - что первично, процесс работы или её результат?

???

Можно сказать, что Go помогает программисту автоматически создавая ветку по умолчанию которая не делает ничего.

Вот ничерта не помогает. Представим что у нас в легаси коде есть енам и свич по нему такого вида

var x string
switch myEnum {
case A: x = "foo"
case B: x = "bar"
}

Мы добавили новый вариант в этот енам и получили баг в легаси коде.

А ситуацию когда есть набор вариантов из которых нужно выбрать ровно один на Go можно оформить массой способов - в Go функции are first class citizens.

Пример? Раз уж их масса

Не "придётся использовать", а "я не могу придумать ничего кроме".

Пример, профессор

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

Во первых, вы не правы - существование инструментов для escape анализа доказывает что думать приходится. Во вторых, в чём достижение?

Сама же идея интересная - кем позиционируется? Где? Когда? Зачем?

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

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

История это не оправдание.

Ну вот... понимают если захотят.

Противное высокомерие. Да, вы в Go такие непонятые гении, никто не ценит ваш чудесный язык.

Не "не можем", а "не должны". Извращаться можно где угодно, но зачем?

Назвать что-то "извращением" это не аргумент.

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

Ну я и говорю, волшебная конструкция прибитая гвоздями

Да, у Go есть история.

История это не оправдание.

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

Не вижу аргумента, вижу ссылку на авторитет.
И вы как бы меня не поняли. Я написал что повсеместное использование any это треш, а не само его наличие (хотя впрочем, может и его наличие это треш, но не буду зарекаться).

Ну не надо такое писать, дурно пахнет. Да, все так пишут, запах только усиливается. В Go перехватывается panic... 

Что вы вообще имели в виду?
В Go и в Rust можно перехватывать панику, но на практике это стоит делать Только в очень специфических случаях. В остальных случаях передача ошибки через возвращаемое значение всегда лучше.

Это уже зашкаливает. Используйте другой тип и всё.

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

То же самое опять. "Обычно" не значит "всегда".

И? В Go принято называть ошибку err, поэтому скорее всего ошибки от разных вызовов будут сидеть в одной переменной

Давно решено. См. := оператор.

Вы о чём? Вы так пишете "см." как будто вы выше написали что-то про оператор :=. Это оператор для создания новой переменной и что? Я додумывать за вас не собираюсь

Спасибо что упомянули. Это как раз то, что было сознательно отвергеуто в Go. Упрощает и без того тривиальное ценой усложнения и так нетривиального.

А, ну да, Go такой неудобный by design, тогда претензии сняты

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

Что вы имели в виду здесь я тоже не знаю

Что лучше, append или .Push - не очевидно, это разные вещи потому, что append добавляет элемент не в массив, а в slice.

По моему очевидно что Push лучше. Если вы хотите отдельный синтаксис для добавления элемента в массив, то сделайте и Push и append.

Не вижу. Создали массив из 10 элементов. Молодцы.

Этот дурацкий синтаксис приводил к багам много раз, я видел это сам. Я рад что вы такой умный и не делаете этой ошибки.

Я бы первым делом заподозрил array literal.

Что вы имели в виду

Не понимаю. У канала нет принимающей и посылающей стороны, есть чтение из канала и запись в канал.

Очевидно у канала есть принимающая и посылающая сторона. И создатели языка тоже так считают, ведь добавили <-chan и chan<-.

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

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

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

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

И не должны мочь. По определению родительский модуль использует дочерний.

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

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

Увы, я не профессор и таланта к обучению не имею.

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

А можете указать конкретные места где я пытаюсь делать "rust в go"? Не отрицаю что такое может быть, но мне кажется что у меня по большей части просто претензии к дизайну Go (по крайней мере в тех частях статьи, где я показываю как это приводит к багам).

у меня по большей части просто претензии к дизайну Go

Я так и написал, да.

Ясно, пояснять вы свой комментарий не будете, так что это пустословие.

Справедливости ради. Не писал ни на go, ни на rust, но в целом согласен с автором статьи. Гораздо лучше, когда язык дает тебе меньше возможностей выстрелить себе в ногу. Можно утверждать, что в тех же ошибках копипасты виноват сам разработчик, однако от этого их не станет меньше, полезно, когда язык снижает вероятность возникновения таких ошибок by design.

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

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

Мне лично было интересно посмотреть на различия в языках с точки зрения человека, который имел опыт разработки на них обоих, спасибо за хорошую статью

Скоро на го накопится легаси, а на раст ещё нет. Тогда перешедший с раст на го будет плеваться, но говорить: "мне платят за разгребание говен".

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

Чтобы была понятно:

Перешёл с C# на PHP год назад. Это как из современной квартиры переехать в дом с "историей".

Go занял нишу (инфраструктура, CLI, микросервисы), но не расширяется. Rust забирает системную часть, Python — ML/скрипты, C# силён в энтерпрайзе и геймдеве.

Go, возможно, достиг потолка — простота языка одновременно и плюс, и ограничение.

Раст по ощущениям может занять и энтерпрайз потихоньку. Лет через десять

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

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

  1. только с помощью стандартной библиотеки

  2. используемые функции/методы не должны принимать any в аргументах

  3. чтобы я мог полученную ошибку привести из error к конкретному типу, но не вызывая руками type assertion

?

Это во первых.

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

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

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

Ваш пакет errors я в другой ветке уже разробрал https://habr.com/ru/articles/980752/comments/#comment_29328330.
Не даёт он нормальных способов для работы с ошибками.

Сравнение на удивление неплохое, глубже чем среднее видео или статья по теме. Но давайте будем честными, Go и Rust уже давно "here to stay" - плотно вошли в индустрию, много нужного софта на них написано, и выяснять, что лучше для ты или иной задачи, уже не так важно. Все равно это будет территория компромиссов.

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

Глубоко не думал об этом, потому что только один раз сталкивался. Но вообще согласен, из-за того что в Go по строке можно свободно брать срез, легко получить невалидную utf-8 строку (для безопасности надо обмазываться всякими DecodeLastRuneInString).

s := "Хелло"
r := []rune(s)

hell := r[:4] // Хелл

Конвертация в []rune выделит память на куче и туда скопирует s? Или это как-то хитрее сделано?

Читал полотно и все ждал абзаца про слайсы, но автор видимо не дошёл :)

Хорошая статья. Спасибо автору.

У меня обратный опыт - использовал Rust после Go.

Могу добавить от себя еще пару моментов:

1) Nil в Go. Option в Rust елегантнее.
2) Cargo сильно лучше чем go.mod
3) Cargo features тоже очень удобная штука.

С другой стороны компилятор у Rust конечно сильно медленне чем у Go.

А еще в Go в стандарной библиотеке (например в reflect) во многих местах понатыкано panic.

В Go мне очень нравится sync.Pool. Есть ли подобное в Rust? Подскажите.

Зато в go в стандартной библиотеке есть https клиент и сервер, криптография и многое другое

И это вовсе не плюс

Я совсем не ругаю Go. Наоборот. У него много плюсов в том числе достаточно хорошая (в целом) std lib.
Планировщик goroutines по моему личному мнению вообще artwork. Даже ребята из tokio брали его за основу, когда делали свой.
За несколько лет использования, как мне кажется, у Go очень четко выделились удачные и неудачные решения. Мне кажется, что большинство согласны с тем, что обработка ошибок, nil, generics, управление памятью и другие моменты могли быть бы лучше. Но изменить это невозможно без нарушения гарантии совместимости v1.0.
Что печально - это то что похоже инициатива Go v2.0 умерла.
Я читал ребят из команды Go, что они не хотят повторить опыт перехода Python v2->v3. Да переход действительно был очень болезненным. Но по прошествие времени мы видим, что это было тяжелое, но правильное решение.

Из интересного, я начинал писать код уже на Python 3.7, в году наверное 18 и ещё тогда (спустя 8 лет после выхода 3й), встречались вакансии с требованиями Python 2.7, но их было на общем фоне не очень то и много. И какого - же было мое удивление, когда знакомый мне рассказал про свой собес в компанию разработчика, толи Вартандер, толи каких-то из танков, в начале 2024. Собес проходил на 2.7
От предложения знакомый отказался, что не удивительно.
Так что переход на тройку, в общем-то, ещё не закончен)

С другой стороны, при наличии LLM, подобный переход можно совершить сильно быстрее и куда менее болезненно, чем это было с Python. Больше того, в Go очень любят кодогенерацию, он довольно строго типизируется, да и даже 10ки лет назад 2to3 справлялся с тем, чтобы оградить разрабов от самой рутинной и простой части.

Так что, как мне кажется, лучше времени для возрождения Go v2 инициативы - уже не будет.

толи Вартандер, толи каких-то из танков, в начале 2024. Собес проходил на 2.7

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

Со слов коллеги, его спрашивали о том как устроена память в 2.7, как работает GC, потоки, процессы, вытесняющая многозадачность и ещё какое-то количество алгоритмических и низкоуровневых штук.
На вопрос - "А зачем вам это все". Ответ - "Часть физики у нас так считается".

А вообще - основная функциональность на Python там, как я понял со слов - в прегейм лобби. Подбор игроков, магазин, таблички, базы.

То чувство, когда сравниваешь самый приближенный к идеалу ЯП с чем угодно, тут даже го будет в пролете. Сравните его с чем угодно другим: с джаваскрепт, плюсами или пыхой - и вы офигеете, какой го отличный ЯП

сорян, но мне джаваскрипт даже лучше чем го. на одну строчку в моем рейтинге но выше)

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

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

Но, как по мне, проще уж MVP писать на Python с высокопроизводительными библиотеками, такими как pydantic, orjson, uvloop, granian, и при этом делать это более выразительно, быстро, чем на Go, либо же сразу писать на Rust, когда действительно нужна максимальная производительность и отказоустойчивость. И между ними Go в данном случае — ни рыба ни мясо.

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

Но, как по мне, проще уж MVP писать на Python с высокопроизводительными библиотеками, такими как pydantic, orjson, uvloop, granian, и при этом делать это более выразительно, быстро, чем на Go, либо же сразу писать на Rust, когда действительно нужна максимальная производительность и отказоустойчивость. И между ними Go в данном случае — ни рыба ни мясо.

Это очень спорное утверждение. Не везде нужно выжимать все капли с железа, а вот скорость разработки в Го на порядок выше. И гарантий по сравнению с питоном тоже на голову выше. Получается как раз золотая середина. У вас однобокий взгляд разраба-перфекциониста, но в жизни нету ничего идеального, всегда нужны компромиссы.

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

Реализация интерфейсов в нормальной IDE видна ещё на уровне редактора. Никогда не ошибался в написании реализации.

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

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

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

IDE и линтер тебе любезно скажут, что ты нехороший человек

Ну да. А можно и через LLM писать и не думать, правда?

IDE это плохие гарантии

А как на бумажке писать на ливкодинге вообще не понятно

С чего вдруг IDE - плохие гарантии, когда практически любой IDE работает в паре с интерпретатором Go

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

Даже собеседование лайвкодил в Idea, без всяких бумажек.

И с какого перепуга IDE - плохая гарантия? Я пользуюсь Idea с плагинами Go и SonarQube, и все там с гарантиями нормально. Все типичные ошибки(речь не о синтаксических) детектируются на ура. А для всего остального у меня есть голова на плечах, как у и любого хорошего разработчика.

Я считаю прекрасно иметь возможность пойти посмотреть исходники стандартной библиотеки, а главное уметь их понять пройдя несколько уроков в gotour. Конечно не хватает "хитрых" типов, банального enum-а. Это создаёт проблемы, но совсем далеко не настолько часто как ошибки в бизнес-логике.

В моей практике не припомню проблемы чтобы кто-то тут добавил энамов, а потом забыл что их добавил. Или решил вместо константы написать просто что `какой-то альяс над int равно 5`. Могут конечно, но врятли такой код переживет ревью, как глазами, так и средствами ИИ.

А вот то что люди могли забыть закрыть какой-то дескриптор при выходе... Ну это может быть и не настолько страшно, поскольку есть литер. А вот если они даже не понимают что этот дескриптор существует и закрывается автоматически при выходе из области видимости... Мне кажется важно понимать что там происходит на низком уровне, понимать как это все работает на уровне ОС. Вероятно далеко не на каждом уровне, и далеко не всем. Го достаточно высокоуровневый по сравнению с Си, но при этом довольно прост и "близок к нему по духу" (очень условно, конечно)).

Вобщем я про "явность" и "очевидность" поведения, которое кажется было туда положено на фундаментальном уровне. От необходимости прочтения "effective go" и "go memory model", помимо го-тура вас ничего не спасет. Когнитивная нагрузка с пониманием эскейп анализа очень быстро исчезает с практикой. Да и потом есть утилита для проверки. Но опять же не помню кейсов где это было критически необходимо. Ну и пример с инициализацией слайса определенной длины - это опять же просто такая конструкция языка которую ты начинаешь видеть сходу, и уже не обращаешь внимание как на этот немного раздражающий ! после println))

чтобы кто-то тут добавил энамов

Так банально новый тип ошибки в обновлённой версии зависимости, нет?

Я вот размышляю над Растом. Чтобы перейти на него. Но после Го отпугивает медленная скорость итераций в связи с медленной компиляцией (от 50 секунд). Как вы с этим справлялись?

Проблема во многом преувеличена.

Текущий проект на 37cloc, компиляция после изменения нескольких строк кода: 1-2 секунды компиляция, 5 сек линковка. Долгая линковка из-за LTO, без него 1 сек.

Компиляция с нуля: ~35 секунд, 5 сек линковка. Но с нуля я запускаю раз в месяц наверное.
Вот для CI/CD в некоторых случаях нужна компиляция с нуля, там немного напрягает.

Долгую компиляцию вызывают несколько библиотек и использований.

1. Разделять код на несколько пакетов. Если есть тяжёлые бинарники (например, веб-сервер -- эти библиотеки все тяжёлые), их можно выделить в отдельные подпроекты, и он не будет проверяться и компилироваться каждый раз. А некоторые стабильные модули можно вынести в отдельные подпроекты тоже, это тоже ускоряет проверку.

2. Некоторые макросы derive генерируют очень много кода, от них желательно тоже избавиться. Например, автоматическая (де)сериализация на Serde (`#[derive(Serialize, Deserialize)]`), вызывает сильное замедление проверки и компиляции. Я отказался от Serde и bincode (сохранял структуры в бинарные файлы) в пользу Zerocopy.

Вот эти две оптимизации позволили сократить время проверки с ~30 секунд до ~3.

Всегда не понимал зачем люди сравнивают тёплое с мягким?

Ну сравните прокатный карт в парке аттракционов и Ламборгини!? Какой смысл?)

Очевидно языки с разным смыслом и философией с таким же успехом я завтра могу написать статью, где сравниваю bash и erlang ...

Спасибо за статью. По большей части с претензиями согласен, пишу на го давно. Из собственного опыта парит например невозможность привести []int to []any(я так понимаю Раст тоже не умеет), невозможность подставить вызов func() (int, int) в accept(a, b int). Из за этого приходится вводить доп.переменные либо всякие lo.* что ухудшает читабельность имхо. Про енумы ни разу не встречал проблем с ними, скорее претензия что реализованы по сути костылём. Про отсутствие дженериков в структурах - да, боль. И непонятно собираются ли это добавлять вообще.

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

[]int to []any(я так понимаю Раст тоже не умеет)

В смысле что привести срез конкретного типа к срезу интерфейсов? Да, раст так тоже не умеет. Хотя потенциал тут есть...Как-будто если мы знаем конкретный тип, то не нужно для каждого объекта в срезе хранить витейбл, достаточно хранить один витейбл на весь срез (то есть создать толстый указзатель на срез).

Невозможность подставить вызов func() (int, int) в accept(a, b int)

В стабильном расте тоже нельзя к сожалению (но вот в нестабильном можно https://play.rust-lang.org/?version=nightly&mode=debug&edition=2024&gist=27f60ae5eee5e5c2ae70a8e8fbe084eb; ну и наверно можно через макросы себе что-то такое навертеть в стабильном).

P.S. что-то ссылка на плейграунд сломалась, я имел в виду что через std::ops::Fs::call можно вызвать функцию с кортежем в качестве параметров.

 что-то ссылка на плейграунд сломалась

В конце ссылки точка с запятой лишняя

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

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

Публикации