Comments 618
>Да, я думаю, что Node на самом деле реально себя показала, как это ни странно, на клиентской стороне. Вроде скриптинга для построения веб-сайтов, или browserify, или bundles для клиентского Javascript кода. Ну или что можно делать всю эту серверную часть обработки клиентского Javastipt-кода. А потом, ну знаете, может быть небольшой сервер, чисто для девелопмента, там и тут, а потом может и настоящий сервер в продакшн, который будет получать реальный трафик. Node может быть полезной, или это может быть просто ваш выбор. Но если вы пишете распределённый DNS сервер, я бы не выбирал Node.
То есть конкретно ОН не выбрал бы ноду для распределенки. Ну ок. От его школьной поделки в современной ноде уже ничего не осталось. Странно вообще видеть среди программистов — оплот логики и здравого смысла — такую тягу к авторитаризму.
Вот тут списочек, сами выберите по вкусу:
Подчинение авторитету
Предвзятость подтверждения
Субъективное придание значимости
Эффект знакомства с объектом
Рационализация после покупки
От его школьной поделки в современной ноде уже ничего не осталось.
Чего конкретно не осталось? Обёртки над V8 для применения JS в обычном окружении современной ОС, а не в песочнице браузера?
На гитхабе — полторы тысячи контрибуторов, вот тут nodejs.org/en/foundation/members мемберы Node.js Foundation. Обертка для v8 — это просто идея, и как идея она простая. Реализация посложнее будет.
Ну, по хорошему, журналисты интервью обычно очищают, да, перефразируют какие-то фразы и т.д. Но это отдельный скилл, да и не хотелось от себя что-то менять, поэтому просто перевод как есть.
там, на самом деле, весь текст оригинала такой.
Одни слова паразиты
Даже когда перевод сейчас почитал местами, сразу вспомнил этот сумбур речи автора
Учиться, учиться и еще раз учиться — это лучше, чем работать, работать и еще раз работать. Ленин.
Всегда говорит Аллах: «Надо учиться, учиться, надо учиться». Мы День знаний не отменили, потому что хотели побольше рассказывать про наш праздник религиозный, и чтобы они [школьники] пришли 1 сентября в школу, которую они ждали, и чтобы побольше был баракат, побольше было всего лучшего для наших детей.
Рамзан Кадыров, глава Чечни
Да, я думаю, что Node на самом деле реально себя показала, как это ни странно, на клиентской стороне.
Буквально год назад задумался о том, а к чему идёт вообще нода и правильно ли её использовать на сервере? Пришёл к выводу, что нода показывает себя сильнее всего на данный момент в фронте и перешёл на го. Как же я, оказывается, был прав.
Ну все, пойду переписывать все свои Node.js бэки на Go. А если серьезно, как-же достала это выдуманная проблема с языками и платформами (особенное с очень близкими по производительности). Т.е. выбор языка по бенчмаркам куда предпочтительнее, чем скорость разработки функционала / экосистема / синтаксис / высокоуровневость?
сделай вот эту штуку А, подожди ответ, может быть упади с ошибкой. Потом сделай штуку Б, подожди ответ, проверь ошибку. И в Node это намного сложнее, потому что ты должен для этого прыгать в вызов другой функции.
async function thingA() {
// magic
}
async function thingB() {
// magic
}
(async function main() {
await thingA(); // И подождет ошибку, и сам упадет
try {
await thingB(); // Сделает штуку Б
} catch (err) {
// Проверит ошибку
}
})()
.catch((err) => {
console.error(err);
process.exit(1);
});
Действительно, это гораздо сложнее и меннее читаемо чем постоянные if err != nil
.
«зеленые потоки»
Event-loop не сравляется? Ок, fibjs, node-fibers, они должны быть чуть побыстрее. (нам ведь это реально нужно).
Но если вы пишете распределённый DNS сервер, я бы не выбирал Node.
А я бы выбрал C, нам ведь важны только бенчмарки, да?
Вы не правильно поняли мой посыл. Я не агитирую ни за Node, ни за Go. Я говорил, что есть вещи куда важнее, чем мериться бенчмарками и бесконечно переписывать с языка на язык (не забывайте, что скоро Rust станет меинстримом, и как заживем, как начнем рефакторить-переписывать :)).
Если подытожить все сказанное выше, пишите на чем вам удобнее и привычнее. Не там много задач, где узким местом будет именно выбранные язык, а не сеть и взаимодействие с диском. А если уж вы и упретесь в один из таких случаев, то нет смысла переписывать всю систему на "+5% к производительности" языке, а лучше задуматься о низкоуровневой реализации этого "горлышка" или же нативном аддоне.
Вы, отчасти, правы, и очень часто, действительно, выбор языка не настолько критичен, как правильный дизайн и понимание проблемной области. Но, всё же, посыл "пишите на чём удобнее" очень уязвим сам по себе и является причиной многих проблем в командах.
Удобнее кому — тим лиду, большинству команды, вам лично? Команды с таким подходом набирают людей "под удобный язык", под скиллы, а не под умение решать задачи и выбирать правильные инструменты. Это, в свою очередь, создает очень предвзятое окружение, в котором сложно развиваться и изучать новые технологии, и все незнакомое кажется "неудобным".
Knowledge debt сам себя не погасит с таким подходом. :)
Удобнее кому — тим лиду, большинству команды, вам лично? Команды с таким подходом набирают людей "под удобный язык", под скиллы, а не под умение решать задачи и выбирать правильные инструменты. Это, в свою очередь, создает очень предвзятое окружение, в котором сложно развиваться и изучать новые технологии, и все незнакомое кажется "неудобным".
Проблема в том, что каждая технология, язык или инструмент содержат много подводных камней и просто вот так выучить их за две недельки не получается. Поэтому нанимателям нужны скиллы, а не мифическая возможность "выбирать правильный инструмент", ведь по факту nodejs и golang, например, абсолютно равнозначны в силу того, что теряют в одном и приобретают в другом. Поэтому выбрать абсолютно правильный инструмент невозможно.
И опять же, полностью согласен с вами в общем случае. Я тут как-то делал перевод статьи одного хаскелиста, который (как это принято) высмеивает Go, но, который, при этом признает, что он может попросить коллегу выучить Go за выходные, но не может это же сделать с Haskell или Purescript.
https://habrahabr.ru/post/270707/
Go это, всё таки, язык, в котором очень многие решения отталкиваются от социального аспекта программирования, коим "порог входа" и является. Он создавался именно с этим расчетом и авторы Go это много раз озвучивали.
Большинство программистов могут освоить язык и самые основные вещи из стандартной библиотеки за выходные (благодаря Go Tour, например). Как правило через неделю новый разработчик, никогда не писавший на Go, уже смело может контрибьютить в проект. Конечно, бывает и дольше, но бывает и быстрее.
Большинство программистов могут освоить язык и самые основные вещи из стандартной библиотеки за выходные (благодаря Go Tour, например). Как правило через неделю новый разработчик, никогда не писавший на Go, уже смело может контрибьютить в проект. Конечно, бывает и дольше, но бывает и быстрее.
Не вижу проблем сделать это же, например, с python, ruby, java, nodejs и кучей других языков. В свое время, когда я только был студентом я осилил python за 15 минут. А вот с go у меня такое не получилось. Например, в силу того, что в отличии от python и golang странные для меня архитектурные решения в плане GOPATH (я тогда еще не знал, как нормально завести GOPATH везде и уж очень намучался с аналогичной проблеме в CUDA) и зависимостей, ну это такое.
Ну и дополнительно:
Как правило через неделю новый разработчик, никогда не писавший на Go, уже смело может контрибьютить в проект.
Эта штука работает только если проект на pure Golang, но в современной разработке очень популярна тенденция фрейворков и как только golang окончательно придет массы (например, с go 2.0), то оно все обернется кучей фрейворков и получится такая же проблема как и с другими языками.
Не вижу проблем сделать это же, например, с python, ruby, java, nodejs и кучей других языков.
Почему? Вам кажется, что у всех языков одинаковая сложность и порог входа?
Если да, то это странная точка зрения.
то оно все обернется кучей фрейворков и получится такая же проблема как и с другими языками.
Ага, некоторые люди прямо так и говорят — "я считаю, что Go ещё сырой, потому что под него мало фрейморков для микросервисов, в отличие от Scala, например".
Но ничего не может быть дальше от истины, чем это утверждение. Это как говорить "я считаю Tesla еще сырая, потому что там нет отверстия для бензобака".
Вы никогда не задумывались, почему создаются фреймворки? По-сути, фреймворк это "язык в языке", для решения специфической проблемы. Прелесть Go в том, что он зародился в 2007-м, в отличие от остальных мейнстримовых языков, которые уже по 20-30+ лет. Он родился тогда и в том месте, где такие вещи как микросервисы/RPC/JSON/криптография и т.д. были жизненной ежедневной необходимостью, поэтому практически всё это есть в стандартной библиотеке.
Тоесть фреймворки в других языках существуют, потому что самого языка и его стандарного набора не хватает. В Go хватает.
Почему? Вам кажется, что у всех языков одинаковая сложность и порог входа?
Если да, то это странная точка зрения.
Нет, у c++, scala, clojure, haskell значительно выше порог. Но это я к тому, что golang не пионер по низкому порогу входа.
Ага, некоторые люди прямо так и говорят — "я считаю, что Go ещё сырой, потому что под него мало фрейморков для микросервисов, в отличие от Scala, например".
Но ничего не может быть дальше от истины, чем это утверждение. Это как говорить "я считаю Tesla еще сырая, потому что там нет отверстия для бензобака".
Вы никогда не задумывались, почему создаются фреймворки? По-сути, фреймворк это "язык в языке", для решения специфической проблемы. Прелесть Go в том, что он зародился в 2007-м, в отличие от остальных мейнстримовых языков, которые уже по 20-30+ лет. Он родился тогда и в том месте, где такие вещи как микросервисы/RPC/JSON/криптография и т.д. были жизненной ежедневной необходимостью, поэтому практически всё это есть в стандартной библиотеке.
Тоесть фреймворки в других языках существуют, потому что самого языка и его стандарного набора не хватает. В Go хватает.
Верно. И где же в golang таки фичи, как:
- ORM
- Построение web приложений при помощи MVC
- Traceback и умная работа с ошибками.
Я думаю, опытные программисты добавят еще чего-то. Суть в том, что Golang не хватает значительного количества вещей, которые нужны для того, что бы писать крупные проекты и не страдать.
Да, для этих вещей уже написали отдельные framework на golang, но проблема в том, что как и для каждой новой технологии необходимо, что бы они поддерживались не только энтузиастами, которые могут в любой момент просто сказать "я устал, я мухожух" и оставить проект в непонятном состоянии, но и какими-то компаниями. Есть ли что-то из первого или второго пункта с такой поддержкой? Я вот не нашел, может вы подскажите.
Я бы исправил вашу аналогию так:
Это как говорить "я считаю Tesla еще сырая, потому что создается энтузиастами в качестве хобби". И это очень похоже на правду.
И где же в golang таки фичи, как:
ORM
Построение web приложений при помощи MVC
Traceback и умная работа с ошибками.
Вы так пишете, будто это что-то абсолютно необходимое, хорошее и уважаемое в программистском мире. Поверьте, всё вышеперечисленное есть, но им обычно пользуются только те, кто никак не может отвыкнуть от "удобного языка".
Я бы исправил вашу аналогию так: Это как говорить "я считаю Tesla еще сырая, потому что создается энтузиастами в качестве хобби". И это очень похоже на правду.
То есть для вас языки, в которых софт комьюнити состоит не из какой-то компании, по определению сырые? Ок, спасибо за (не очень) интересную дискуссию. Надеюсь ваша IDE, написанная компанией, уже запустилась, давайте работать.
Вы так пишете, будто это что-то абсолютно необходимое, хорошее и уважаемое в программистском мире. Поверьте, всё вышеперечисленное есть, но им обычно пользуются только те, кто никак не может отвыкнуть от "удобного языка".
А вы предлагаете работать с базой при помощи чистых sql запросов? Это мало того, что не удобно, это еще делает ваш код базо-ориентированным и приводит к куче проблем с производительностью, безопасностью и прочему. Да, программисты не любят orm за странные вещи и то, что некоторые части таки приходится переписывать на чистый sql. Но писать все приложение на sql — это безумие.
То есть для вас языки, в которых софт комьюнити состоит не из какой-то компании, по определению сырые? Ок, спасибо за (не очень) интересную дискуссию. Надеюсь ваша IDE, написанная компанией, уже запустилась, давайте работать.
Компании, фонды, разработчики, которых отдельно спонсируют. Без них у вас вообще нет гарантии, что библиотека, которую вы используется в один момент не будет заброшена и ее поддержка не ляжет на ваши плечи. Если вы большая компания, вы можете себе это позволить. Но не все компании большие.
Я как разработчик могу писать на чем угодно, но для компании использовать такие инструменты довольно небезопасно. Я не прав?
это еще делает ваш код базо-ориентированным
А каким он ещё должен быть? Если вы используете базу, значит работаете с данными.
Да, программисты не любят orm за странные вещи и то, что некоторые части таки приходится переписывать на чистый sql. Но писать все приложение на sql — это безумие.
Понимаете, какая дилемма: ORM не лажает в простых случаях, в которых написать на чистом SQL тоже не так сложно (хоть и дольше), а в сложных случаях всё сложно: хоть на SQL, хоть на ORM. В общем, как всегда: магии не случилось и ты получаешь то, за что платишь (в данном случае знаниями и трудом).
Это мало того, что не удобно, это еще делает ваш код базо-ориентированным и приводит к куче проблем с производительностью, безопасностью и прочему.
Вот это уже полная ерунда. Все нормальные библиотеки для работы с базами предохраняют от SQL-injection на 100%, а грамотно написанный SQL ВСЕГДА быстрее работает, чем запросы от ORM.
Понимаете, какая дилемма: ORM не лажает в простых случаях, в которых написать на чистом SQL тоже не так сложно (хоть и дольше), а в сложных случаях всё сложно: хоть на SQL, хоть на ORM. В общем, как всегда: магии не случилось и ты получаешь то, за что платишь (в данном случае знаниями и трудом).
Вы говорите так, как будто ORM работает как LINQ, вы упускаете вот такие штуки:
- ORM следит за структурой базы данных
- ORM предоставляет детальную информацию про структуру бд, которую можно использовать, например, для формирования ответов, запросов или еще чего-то (я, например, команды конфигурации так делают).
- ORM позволяет добавлять к модели логику, которую в ином случае не совсем понятно где хранить.
И это все на чистом SQL делается только через страдания и шаблонный код. Ну или вы реализуете свою ORM.
Вот это уже полная ерунда. Все нормальные библиотеки для работы с базами предохраняют от SQL-injection на 100%, а грамотно написанный SQL ВСЕГДА быстрее работает, чем запросы от ORM.
- Для сложных случаев грамотно написать SQL на проекте может, ну, 20% девов, так что лучше ORM.
- Подскажите, как выбора по id для всех полей может быть написана быстрее, чем
select * from table where id=<id>
? Однострочники, которые генерирует ORM вряд ли будут сильно быстрее. Разве что у вас большая таблица и вы заранее знаете, какие поля будете использовать. А потом будете страдать и каждый раз дописывать поле в запрос. - psycopg2 нормальная? Для передачи параметров там нужно использовать специальную функцию, потому что простое
cur.execute(query)
не делает экранирования (что логично), а значит, если вы будете формировать запрос в обход предоставленного интерфейса, то у вас будет дыра.
- В ОРМ как правило есть только одна модель сущности — из-за этого запросы возвращают все поля, даже если реально нужно одно-два
- Бутстрап ОРМ в базе с большим числом обьектов может занять приличное время из-за чтения метаданных
- Если вам нужны hint-ы, тем более разные у разных клиентов — это проблема
- В ОРМ как правило есть кэш которым надо управлять если базой пользуется кто-то еще
- Управление транзакциями в ОРМ может быть довольно нетривиальным
Для сложных случаев грамотно написать SQL на проекте может, ну, 20% девов, так что лучше ORM.
Одного решения для всех случаев нет.
В сложных случаях на больших проектах программист идёт к DBA и рыдает в жилетку, а тот его выслушивает, делает чашку кофе, и пишет километровое CREATE VIEW (или, в особо сложных случаях, хранимую процедуру, возвращающую датасет). После чего получившееся уже можно использовать как угодно, хоть напрямую, хоть через ORM.
Сложный случай по определению выходит за пределы того, что может средний коробочный ORM в стандартной конфигурации. А если он такое и сможет, то план запроса будет отличаться от оптимального, как котлета от истребителя.
Но не все ORM одинаково плохи для всех задач. Сайт с тремя новостями и пятью посетителями можно делать на чём угодно, хоть на тикле и текстовых файликах вместо базы данных, лишь бы это было удобно писать и сопровождать. Но на таком проекте и сложным случаям взяться неоткуда, там всё в пределах возможностей любого ORM десятилетней давности.
P. S. psycopg2 абсолютно нормальная, просто не надо давать её использовать напрямую. Откройте к ней фиксированный интерфейс и принимайте только параметризованные запросы.
Яркий повод — вспомогательные таблицы для отношений Many-To-Many, где ID не нужен, а первичным ключом может служить комбинация foreign key
Спорный вопрос, ведь есть же нормальные формы (если я вас правильно понял), зачем-то их придумали.
Если у вас такой проект, то может стоит выкинуть реляционку и добавить что-то документно-ориентированное?
Пример хоть одной такой таблицы сможете привести, где для идентификации сущностей достаточно естественного ключа?
Натуральные (иногда композитные ключи) могут заменить суррогагные почти всегда. Исключение — личные данные людей (даже комбинация фамилия, имя, отчество, дата_рождения не может по-хорошему считаться уникальной) или когда натуральный ключ получается совсем уж развесистым, что скажется на производительности.
Например, есть таблица authors и таблицы books.
Если таблица authors однозначно сопоставима с таблицей persons (где хранятся имена/фамилии людей и уже есть суррогатный ID по причинам, описанным выше), то первичным ключом для authors может быть foreign key на persons.
Для таблицы books можно взять в качестве первичного ключа ISBN (он сюда так и просится) или комбинация дата_публикации, название или что-то ещё.
Тогда таблица books_by_authors, суть которой заключается в двух foreign key на authors и на books может спокойно иметь первичный ключ, являющийся комбинацией этих foreign key.
В итоге суррогатные ID будут только у самых базовых сущностей (и то не у всех), а все остальные будут иметь натуральные (иногда композитные) ключи
"Для изданий, выходящих малым тиражом (в издательской практике 2003 года — до 1 000 экземпляров), либо для «личного» использования присваивать номер ISBN необязательно."
"На издании могут стоять два и более международных стандартных книжных номера"
"Международная стандартная нумерация книг не распространяется на" и там длинный список.
https://ru.wikipedia.org/wiki/Международный_стандартный_книжный_номер
Давайте ещё пример.
Для сложных случаев грамотно написать SQL на проекте может, ну, 20% девов, так что лучше ORM.
я бы сказал так: 20% разработчиков понимают, какой ужас делает ORM и могут написать в разы быстрее. Остальные — пишут примерно на уровне ORM.
Хотите скорости — пишите на SQL, хотите гибкости в ущерб скорости — используйте ORM. Ну и отсылаю к замечательной книжке бывшего коллег "Дефрагментация мозга".
А можно получить и скорость и гибкость, если не цепляться за РСУБД, а взять графовую СУБД.
Вы как-то противопоставляете ORM и вручную написанный SQL. ORM ничего не говорит о том как получается SQL при работе с объектами. Как-то. Хотите пишите вручную, хотите — генерируйте каким-нибудь квери-билдером на основе метаданных из ОРМ.
это еще делает ваш код базо-ориентированнымА каким он ещё должен быть? Если вы используете базу, значит
работаете с данными.
Проблема в том, что код становится ориентированным на конкретную базу данных. Захотел сменить MySQL на Postgres? Переписывай большинство запросов.
грамотно написанный SQL ВСЕГДА быстрее работает
… только если его пишет специалист с опытом работы с конкретной базой данных, разбирается в планах и индексах и вообще полу-DBA. Запрос, работающий быстро в Firebird, тормозит как черепаха в MSSQL, например. И наоборот.
Все нормальные библиотеки для работы с базами предохраняют от SQL-injection на 100%
Только до тех пор, пока программист не пытается программно сгенерировать сам SQL.
Захотел сменить MySQL на Postgres? Переписывай большинство запросов.
И как часто вы меняли базу?
А вы предлагаете работать с базой при помощи чистых sql запросов?
Именно. Попробуйте, это не так сложно, как кажется, но в разы более надежно, особенно когда приходится отлаживать проблемы. ORM очень спорная штука — далеко не всегда базу можно красиво "замаппить" на объектную модель языка. SQL не так страшен, как вам может казаться.
Без них у вас вообще нет гарантии, что библиотека, которую вы используется в один момент не будет заброшена
И что? У вас тоже нет гарантии, что компании, которые вы так тут хвалите (мы все знаем, на какую компанию вы намекаете, но опустим этот момент), не обанкротятся и не забросят библиотеки. Абсолютных гарантий нет, но open-source коммьюнити оказывается часто гораздо более живучи и адекватны многих "компаний". Если следовать вашей логике, то Linux как явление вообще не могло существовать — нет же гарантий, что разработчики-энтузиасты не забросят его. А он есть.
но для компании использовать такие инструменты довольно небезопасно. Я не прав?
Вы, скорее, пытаетесь подтвердить свой confirmation bias, а не разобраться и судить о проблеме объективно.
Именно. Попробуйте, это не так сложно, как кажется, но в разы более надежно, особенно когда приходится отлаживать проблемы. ORM очень спорная штука — далеко не всегда базу можно красиво "замаппить" на объектную модель языка. SQL не так страшен, как вам может казаться.
Я попробовал. После очередной порции шаблонных конструкций в духе "написать большой запрос, в котором меняется только одна часть" -> "выполнить запрос" -> "загрузить это в представление" я это все завернул в самописный полуручной orm. Да, изначально я попробовал писать без orm, потому что они не подходили под мою идею, но потом пришлось самому что-то похожее написать.
Потому что это избавляет от сотен строк шаблонного текста.
И что? У вас тоже нет гарантии, что компании, которые вы так тут хвалите (мы все знаем, на какую компанию вы намекаете, но опустим этот момент), не обанкротятся и не забросят библиотеки. Абсолютных гарантий нет, но open-source коммьюнити оказывается часто гораздо более живучи и адекватны многих "компаний". Если следовать вашей логике, то Linux как явление вообще не могло существовать — нет же гарантий, что разработчики-энтузиасты не забросят его. А он есть.
Компанию дают хоть какую-то гарантию. Например, spring и django курируются компаниями. Linux для бизнеса и не существовало, пока не появились такие штуки как Linux Fundation, Canonical, RedHat. А гарантом того, что они не обанкротятся служат другие компании, которые их спонсируют и используют их продукты.
Open-source часто оказывается, а часто и не оказывается. И играть в такую лотерею вроде бизнесу не хочется?
Вы, скорее, пытаетесь подтвердить свой confirmation bias, а не разобраться и судить о проблеме объективно.
Возможно, вы меня немного не поняли. Я говорю не только о компаниях, которые непосредственно продают продукт, но так же о различных фондах СПО, разработчиках, которых спонсируют компании и прочее. Они дают дополнительную гарантию того, что продукт не будет заброшен.
Вот есть шикарная библиотека bumpversion, которая отлично работает и решает некоторые проблемы версионности и которая… не поддерживается с 2015 года. И много людей ее использует, даже делают форки, но в силу того, что pypi пакет никто не может отозвать ее развитие остановилось навсегда. И таких примеров можно найти достаточно.
И таких примеров можно найти достаточно.
Вы просто демонстрируете мой поинт. Вместо того, чтобы разобраться в теме, вы типичным cherry-picking-гом (это форма confirmation bias-а) находите пример, подтверждающий вашу точку зрения, и выдаете его за "доказательство". Если бы вы хотели разобраться объективно в теме — вы бы поискали библиотеки, которые не были заброшены, посчитали бы статистику и сделали выводы.
Скучно.
Когда у вас нет хватает аргументов, вы начинаете применять не очень честные приемы риторики?
Чем мой confirmation bias хуже вашего? У вас так же нет статистики, данных или еще чего-то.
Причем у меня это даже не confirmation bias, так как мой поинт в том, что полезные, крутые и удобные open-source проекты, которые не пользуются поддержкой спонсоров могут быть запрошены (а есть ли пример того же, но с поддержкой спонсоров, который пропал не по схеме "денег становилось все меньше и меньше?). А это дополнительные риски для бизнеса, так как тогда нужно будет выделять людей, которые будут поддерживать это решение и исправлять в нем недочеты.
Чем мой confirmation bias хуже вашего?
Тем, что вы его подкармливаете и лелеете, а я со своими борюсь активно.
мой поинт в том, что полезные, крутые и удобные open-source проекты, которые не пользуются поддержкой спонсоров могут быть запрошены
А могут и не быть. Это не поинт и не аргумент вообще. Это фраза, чтобы разжечь спор дальше, на которой вы строите свое доказательство, что Go нельзя использовать в продакшене. Не пойму только, зачем вы тратите столько своего и чужого времени на эту бессмыслицу.
Ну окей, давайте на конкретном примере.
Вот вы CTO крупной компании и начинаете новый проект. Это будет еще один web проект, с кучей интересных штук. Проект планируется на 5 лет.
Вы можете взять известный вам уже язык (например, python) и знакомый стек технологий django + celery, который точно знаете, что еще довольно долго будут на плаву хотя бы по тому, что их используют и спонсирую крупные компании.
С другой стороны, у вас есть golang, в случае которого, если вам нужен ORM, то у вас по факту есть только xorm, так как другие проекты ведут в целом по одному человеку. Если вам нужна очередь отложенных задач, то у вас в целом нет проектов, которые бы поддерживали хотя бы группы людей, а не один человек.
И есть с этим одним человеком, которого вы даже не знаете, что-то происходит, например, он теряет интерес или у него появляется жена/ребенок, например, ваш крупный проект из нормального сразу переходит в статус веселенького, так как с этим фреймоворком, который используется во всем проекте, вы остались один на один. А править ошибки в нем тоже надо.
И это реальная проблема, делать вид, что ее не существует или она не существенная довольно странно. Да, она не всегда является решающей, но если вы пишите крупный проект, то именно из-за этой проблемы вам придется или хорошенько вложится в Golang, или все-таки параллельно взять другой язык.
Если вы как CTO свели выбор платформы к вашим субъективным оценкам гарантий риска смертности авторов ORM библиотеки (которую вы сами нагуглили и решили, что она единственная), то такой из вас себе CTO. :)
Да, она не всегда является решающей, но если вы пишите крупный проект, то именно из-за этой проблемы вам придется или хорошенько вложится в Golang, или все-таки параллельно взять другой язык.
- Я указал, что этот вопрос не единственный.
- Я нашел где-то штук 7. Можно глянуть тут. Одна перешла к фреймворк (которых, как вы считали, go не нужно), еще одна вроде как не orm вовсе. А остальные поддерживаются одним человеком, я написал, что сделал выбора исходя из этого.
- Это не субъективные оценки. Это называется bus factor. И очень грустно, когда он внезапно не зависит от компании вовсе.
- Ну и самое главное. Можно было еще заметить, что я упомянул еще task queue, положение которых в golang еще хуже. Сказали бы хотя бы, что я свел выбор платформы к двум факторам, а это получается какой-то confirmation bias.
О нет, укажите, пожалуйста, раз вы видите.
Мне вот кажется, что все логично. Строить долгосрочные проекты на продуктах, поддержки которых вам никто не гарантирует довольно печальное занятие.
В чем тут ошибка?
- язык это не ORM
- гарантий абсолютных нет нигде
- нужные экосистеме библиотеки, которые забрасываются — это исключение
- это одинаково справедливо для любых языков
- и даже эти проблемы решаемы
Игнорировать сотню других аспектов разработки "большого продукта в компании" сводя это к "я не могу дать 100% гарантию, что ORM на этом языке его разработчик не забросит когда-нибудь" это как говорить "я не буду контрибьютить в опен-сорс, потому что нет гарантии, что гитхаб не ляжет и мой коммит не увидят.
Это же азбука логики.
гарантий абсолютных нет нигде
Да? Вот я знаю, что Linux не загнется, потому что есть linux foundation, я знаю, что django тоже останется на плаву, потому что есть Django Software Foundation, я знаю, что с golang будет все в порядке, потому что есть Google, который его использует.
А кто работает с task queue на golang? Как много компаний используют orm? Большинство проектов вообще не используют sql, а всякие kv базы, как kubernetes, docker и так далее.
язык это не ORM
Язык это еще и инфраструктура, выработанные решения и фреймворки.
Вот я знаю, что Linux не загнется, потому что есть linux foundation
Представляю как бы вы разглагольствовали до 2000, пока не было Linux Foundation :)
А кто работает с task queue на golang?
Все. А в ваших привычных языках для этого разве нужны отдельные библиотеки? :)
Большинство проектов вообще не используют sql, а всякие kv базы
Right tool for the right job. Или вы все пишете через ORM, там где даже не нужна реляционная модель данных?
Язык это еще и инфраструктура, выработанные решения и фреймворки.
Ага. Проблема-то в чём? Угадайте сколько за 4 года использования Go тысячами компаний в мире библиотек перестало поддерживаться и похоронило проект?
Все. А в ваших привычных языках для этого разве нужны отдельные библиотеки? :)
А в golang отложенная очередь идет из коробки? зачем тогда вот этот проект? Ну и несколько таких же.
Right tool for the right job. Или вы все пишете через ORM, там где даже не нужна реляционная модель данных?
Вы сместили акцент моего комментария в другую сторону. Суть была в том, что самые известные проекты на go не используют orm, так что он вроде как и не нужен для инфрастуктуры то особо.
Ага. Проблема-то в чём? Угадайте сколько за 4 года использования Go тысячами компаний в мире библиотек перестало поддерживаться и похоронило проект?
Думаю, тысячи. И это только не известных. Загляните, например, вот сюда и посмотрите на количество проектов, которые уже не поддерживаются год или два. А это 9 из 17 проектов, если я правильно посчитал. Проекты, которые не получают спонсоров довольно часто пропадают, когда у автора пропадает желание им заниматься. И к сожалению, это естественно.
Представляю как бы вы разглагольствовали до 2000, пока не было Linux Foundation :)
А до Linux Foundation был redhat. Думаю, два года люди как-то пожили без linux)
зачем тогда вот этот проект?
Это распределенный task queue c кешем и стораджем.
Думаю, тысячи.
посмотрите на количество проектов, которые уже не поддерживаются год или два
Понятно. А вам не приходило в голову, что в Go проекты, написанные 5 лет назад компилируются и работают и сегодня (что почти нереально в других языках), и проект, который не обновлялся несколько лет не означает автоматически что он перестал работать или стал хуже. Это в вашем мире так, но не в мире Go. Жаль, что вы судите из своего пузыря и еще пытаетесь этим что-то кому-то доказывать и тратить чужое время.
что почти нереально в других языках
Простите, что? Вы когда-то использовали какие-то языки помимо go или только страшные статейки читали?
и проект, который не обновлялся несколько лет не означает автоматически что он перестал работать или стал хуже
Это значит, что в проекте не происходит исправление ошибок, а еще значит, что он никак не развивается. Или каждый проект написанный на Golang отливается в граните и является идеальным с самого начала?
Мне вот кажется, что все логично. Строить долгосрочные проекты на продуктах, поддержки которых вам никто не гарантирует довольно печальное занятие.
Гарантий нет, даже, если вы купите какой-либо фреймворк/библиотеку/продукт. У вендоров даже есть опция: при покупке какого-либо продукта предусматривается доступ к исходным кодам, если компания-продавец почила в бозе.
А для Go идеология Google — развивать и затягивать из открытых исходников к себе. А не развивать у себя, и открывать уже вовне.
А для Go идеология Google — развивать и затягивать из открытых исходников к себе. А не развивать у себя, и открывать уже вовне.
И это совершенно отвратительно. Потому что тебе потом хочется предложить человеку новый фунционал, а у него библиотека docker-api отстает на несколько версий, как вот тут.
У них там gc менялся вроде)
Какое это отношение имеет к тому, что код написанный и оттестированный 2 года назад, по вашей логике, перестает быть жизнеспособным сегодня без изменений? Просветите.
Никакого, это была штука в ответ на шутку. Проблема не поддерживаемого кода исключительно в том, что в него не вносят исправлений и баги, которые там есть остаются там годами. Включая и баги с производительностью, утечками и прочими прелестями.
Если этой простой проект, который делает одну функцию, то его код может не меняется годами. Но если это сложный проект, например, orm, то тут все становится хуже.
Он может начать дико тормозить или падать с нехваткой памяти.
И это совершенно отвратительно. Потому что тебе потом хочется предложить человеку новый фунционал, а у него библиотека docker-api отстает на несколько версий, как вот тут.
Что конкретно отвратительно? Вести разработку библиотек в открытом виде?
Если нет готового, сделайте форк этой утилиты — допишите API. Мне понадобился gowsdl, я просто поправил под себя.
Вы никогда не задумывались, почему создаются фреймворки? По-сути, фреймворк это «язык в языке», для решения специфической проблемы.
Фреймворк — это как IDE, он объединяет несколько/множество инструментов для решения разнообразных задач. И совсем не обязательно, чтобы эти инструменты были необходимой частью фреймворка, напротив, если он не содержит этих инструментов, а служит лишь интерфейсом, то это вообще замечательно.
Тоесть фреймворки в других языках существуют, потому что самого языка и его стандарного набора не хватает. В Go хватает.Фреймворки существуют, потому что делают процесс разработки быстрее и удобнее (Осторожно, вызывает привыкание). Если вам хватает стандартного набора Go, то зачем использовать «левые» пакеты?
Кстати, по вашему примеру кода — чисто субьективные впечатления после Go:
- async/await просто лишний когнитивный балласт (в сравнении с Go, опять же)
- без комментария не понятно, что произойдет при ошибке в thingA(), пока не проскроллишь глазами в самый низ (и это на микроскопическом примере уже не очень приятно)
try { } catch (err) { }
конструкция гораздо более громоздка, голословна, и при этом менее ясна, чемif err := thingB(); err != nil {... }
. 5 строк против 3, при нулевых бенефитах в данном случае, и еще и спрятанной магией передачи ошибки.- снова же, приходится бегать глазами от начала main до конца, чтобы понять, точно ли не упустил ничего из этого flow-а программы, раскиданным в нескольких местах.
Ну это я так, набросил. Когнитивная нагрузка у JS таки выше.
try { } catch (err) { } конструкция гораздо более громоздка, голословна, и при этом менее ясна, чем if err := thingB(); err != nil {… }. 5 строк против 3, при нулевых бенефитах в данном случае, и еще и спрятанной магией передачи ошибки.
Справедливости ради есть вот такая штука, которая значительно удобнее err !=nil. Более того, вы упускаете тот факт, что err!=nil вам нужно будет пихать после каждого вызова, который вам нужно обрабатывать, а в try блок можно завернуть большое количество строк кода и отловить ошибку в самом конце.
снова же, приходится бегать глазами от начала main до конца, чтобы понять, точно ли не упустил ничего из этого flow-а программы, раскиданным в нескольких местах.
Разве так будет не в любом случае, если у вас main метод не представляет собой целую функцию без использования других? А если вы можете запомнить что они делают и так, то цепочка вызовов и там, и там.
Более того, вы упускаете тот факт, что err!=nil вам нужно будет пихать после каждого вызова, который вам нужно обрабатывать, а в try блок можно завернуть большое количество строк кода и отловить ошибку в самом конце.
Как правило, если мне в одной функции приходится делать больше 3-х подобных однотипных операций (или одни и тех же), это признак плохого (ре)дизайна и само собой наталкивает на его пересмотр.
Такие ситуации, действительно, имеют быть, но они чаще исключение, чем норма, да и для них в Go можно сделать красивые решения.
А вот в вашем примере это возможность "завернуть большое количество строк кода" абсолютно ни к чему. Зато приводит к следующему:
- уменьшает локальность кода — вам нужно прыгать глазами вниз функции, чтобы вообще узнать, как обрабатывается ошибка и обрабатывается вообще, при этом держать в памяти вызов, пока вы прыгаете (когнитивная нагрузка)
- прячет от вас ясность того, что происходит и какой оверхед от передачи ошибки (она ж таки где-то там передается сама при throw, но у вас нет шанса это узнать, кроме как изучить внутренности устройства exceptions в JS)
- прячет от вас понимание того, вообще возвращает функция ошибку или нет
- создает лишнюю нагрузку на производительность и расход памяти (вам всего-то надо передать одну переменную, а рантайм там целые стектрейсы в фоне гоняет, выделяя память, и никакой видимости этого нет)
- ну и менее очевидное, но очень важное (и мое любимое), заставляет вас относится к ошибкам, как к "не сильно важному коду, который нужно засунуть подальше".
И вот в этом двухстрочном примере у вас же и выбора нет. Вы действительно должны использовать этот 5 строчный громоздкий блок с кучей проблем, потому что "удобный вам язык" предоставляет только такую модель. А в Go при желании можно сделать что угодно — "ошибки" это просто интерфейсные переменные. Можно даже try..catch… реализовать для тех кейсов где это нужно, но оно правда лишнее.
Такие ситуации, действительно, имеют быть, но они чаще исключение, чем норма, да и для них в Go можно сделать красивые решения.
Но почему же тогда так не поступили вот тут, например? Или вот тут?
уменьшает локальность кода — вам нужно прыгать глазами вниз функции, чтобы вообще узнать, как обрабатывается ошибка и обрабатывается вообще, при этом держать в памяти вызов, пока вы прыгаете (когнитивная нагрузка)
Зато увеличивает бессмысленность кода. Это отлично, когда каждая ошибка обрабатывается умно и отдельно, но мне кажется, что 99% ошибок в golang просто выкидывают в лог или прокидывают дальше.
прячет от вас ясность того, что происходит и какой оверхед от передачи ошибки (она ж таки где-то там передается сама при throw, но у вас нет шанса это узнать, кроме как изучить внутренности устройства exceptions в JS)
А так вам нужно изучать оверхед блока if + возврата дополнительных аргументов и их распаковку.
прячет от вас понимание того, вообще возвращает функция ошибку или нет
Спасает документация или такие штуки как в Java. Более того, документация вида raises в python мне нравится больше, так как она дает понимание какого рода ошибка вернется, чего не дает простой err в golang. Ну и да. Как вообще вот эта функция работает, если там в конце возвращает одна переменная, а не две? иногда ошибка, иногда нет?
создает лишнюю нагрузку на производительность и расход памяти (вам всего-то надо передать одну переменную, а рантайм там целые стектрейсы в фоне гоняет, выделяя память, и никакой видимости этого нет)
Ну, а так вы пробрасываете ошибку и теряете весь traceback. На мой взгляд информация про ошибку значительно важнее мифических 4-8МБ.
ну и менее очевидное, но очень важное (и мое любимое), заставляет вас относится к ошибкам, как к "не сильно важному коду, который нужно засунуть подальше".
Эм… а вы уверены? У вас так же 99% ошибок выкидывается. Как видите, это не помогло.
И вот в этом двухстрочном примере у вас же и выбора нет. Вы действительно должны использовать этот 5 строчный громоздкий блок с кучей проблем, потому что "удобный вам язык" предоставляет только такую модель. А в Go при желании можно сделать что угодно — "ошибки" это просто интерфейсные переменные. Можно даже try..catch… реализовать для тех кейсов где это нужно, но оно правда лишнее.
В других языках так можно делать тоже. В python их можно просто игнорировать, в java можно сделать метод для экранирования. А golang заставляет вас эмулировать traceback без самого traceback своими силами. Возможно идеология за этим стояла другая, но получилось у людей так. И какой тогда в этом смысл?
Но почему же тогда так не поступили вот тут, например?
Там вполне ок код — три возврата это не проблема. Мне бы было интересно, кстати, как бы вы переписали второй пример на эксепшенах :)
Я как-то слышал аргумент про возвращаемые значения от людей, которые не сильно поняли разницу между возвращаемыми ошибками в Go и С: "Тот, кто работал с возвращаемыми кодами ошибок в ядре Linux, больше никогда такого не захочет". А потом попытался представить как бы ядро Linux выглядело, если было бы написано с помощью эксепшенов. Думаю, не было бы у нас Linux.
Это отлично, когда каждая ошибка обрабатывается умно и отдельно,
Именно об этом и речь.
но мне кажется, что 99% ошибок в golang просто выкидывают в лог или прокидывают дальше.
И супер. А что еще делать с ошибками? Это самые популярные варианты, разумеется. Но это не просто "прокинуть дальше" — это логичный и понятный flow программы для программиста, который читает код. Не забыайте, что язык программирования это не столько язык для общения человека с компьютером, сколько человека с человеком.
А так вам нужно изучать оверхед блока if
И какой-же там оверхед? :) Одна CMP инструкция?
Спасает документация
Безусловно, это не была нерешаемая проблема. Можно написать специальные IDE для языка, которые будут это делать, можно даже автору кода позвонить и спросить, что функция должна вернуть. Только это высокая стоимость для такой обыденной операции, которую делаю миллионы программистов много раз за день. Фред Брукс это называет "добавленной сложностью" — которую мы сами приносим с новыми языками/технологиями. Её нужно уменьшать как только можно.
Ну, а так вы пробрасываете ошибку и теряете весь traceback.
Traceback далеко не всегда нужен. В Питоне например стектрейс выводит при каждом чихе и это тупость. Пользователю нужно возвращать удобночитаемое сообщения, а стектрейс нужен для девелопера в ситуациях, когда действительно что-то свалилось в панику, что Go и делает автоматически. Плюс, благодаря тому, что в Go нет наследования, уровни иерархии даже в больших Go программах относительно невелеики, поэтому часто достаточно в логгере печатать file:line и не гонять дорогие стек-трейсы туда-сюда. Go всё таки часто для высоконагруженных систем используется и этот оверхед ни к чему.
У вас так же 99% ошибок выкидывается.
Вы всё ещё считаете, что "проброс ошибки наверх" это лишняя операция, от которой нужно избавиться? Про локальность и понятность кода я уже выше писал, не хочу повторяться.
А golang заставляет вас эмулировать traceback без самого traceback своими силами.
Опять же, если нужен стектрейс, используют либо panic()
(fail fast), либо debug.PrintStack()
. Можно и просто в переменную error стек сохранять, есть удобные либы для этого. В Go есть выбор, гибкость и очень разумные дефолтные значения.
Там вполне ок код — три возврата это не проблема. Мне бы было интересно, кстати, как бы вы переписали второй пример на эксепшенах :)
Мне довольно сложно понять, что именно там происходит, но, например, на python это будет просто цикл через os.walk и raise из цикла.
Я как-то слышал аргумент про возвращаемые значения от людей, которые не сильно поняли разницу между возвращаемыми ошибками в Go и С: "Тот, кто работал с возвращаемыми кодами ошибок в ядре Linux, больше никогда такого не захочет". А потом попытался представить как бы ядро Linux выглядело, если было бы написано с помощью эксепшенов. Думаю, не было бы у нас Linux.
Я думаю, надо писать статью "Ваше веб-приложение не ядро Linux".
И супер. А что еще делать с ошибками? Это самые популярные варианты, разумеется. Но это не просто "прокинуть дальше" — это логичный и понятный flow программы для программиста, который читает код. Не забыайте, что язык программирования это не столько язык для общения человека с компьютером, сколько человека с человеком.
То есть? Это два варианта "обработать тут" или "прокинуть дальше". Оно так работает везде, но только golang заставляет вас выбирать каждый раз, а не дает возможность выбрать поведение по умолчанию.
Traceback далеко не всегда нужен. В Питоне например стектрейс выводит при каждом чихе и это тупость. Пользователю нужно возвращать удобночитаемое сообщения, а стектрейс нужен для девелопера в ситуациях, когда действительно что-то свалилось в панику, что Go и делает автоматически. Плюс, благодаря тому, что в Go нет наследования, уровни иерархии даже в больших Go программах относительно невелеики, поэтому часто достаточно в логгере печатать file:line и не гонять дорогие стек-трейсы туда-сюда. Go всё таки часто для высоконагруженных систем используется и этот оверхед ни к чему.
- А в каких еще случаях должны выводится ошибки, если не в тех, когда разработчику нужен traceback?
- Вложенность функций все равно остается. И не совсем представляю, как можно оставить маленькие читаемые функции, маленький уровень их вложенности и при этом писать сложные приложения.
- Java тоже используется в таких случаях, и ей почему-то все "кчему".
Вы всё ещё считаете, что "проброс ошибки наверх" это лишняя операция, от которой нужно избавиться? Про локальность и понятность кода я уже выше писал, не хочу повторяться.
Нет, я считаю, что ради 1% случаев не имеет смысла не делать этот проброс автоматическим.
Опять же, если нужен стектрейс, используют либо panic() (fail fast), либо debug.PrintStack(). Можно и просто в переменную error стек сохранять, есть удобные либы для этого. В Go есть выбор, гибкость и очень разумные дефолтные значения.
panic кладет всю программу, что довольно странно, например, для веб-сервера. Ну и понятно, что своих костылей можно накрутить много, но хотелось бы это все из коробки.
очень разумные дефолтные значения
Для узкого круга задач. Но вы же начали толкать язык в массы, а он, оказывается, заточен под узкий круг задач, а для всего остального нужно писать обертки.
А в каких еще случаях должны выводится ошибки, если не в тех, когда разработчику нужен traceback?
Например "порт занят" или "db/users.go:45: ошибка в SQL: такая-то такая то".
Если не задумываясь считать, что стектрейсы нужны везде — это печально.
а не дает возможность выбрать поведение по умолчанию.
Именно. Уменьшая когнитивную нагрузку для того, кто будет читать это код потом.
Я думаю, надо писать статью "Ваше веб-приложение не ядро Linux".
Go это не веб-фреймворк.
Если не задумываясь считать, что стектрейсы нужны везде — это печально.
Мне кажется, вариантов где они нужны, все-таки больше. Хотя тут нужна какая-то статистика.
Именно. Уменьшая когнитивную нагрузку для того, кто будет читать это код потом.
Разве? А на мой взгляд только увеличивает, так как теперь куча лишних строк для прокидывания ошибок.
Go это не веб-фреймворк.
А для чего он тогда нужен? Драйвера все равно будут писать на C, десктопные штуки на нем писать точно не будут, остаются только системные утилиты, которые пишутся на том, что получится и web-сервисы. Ну и embedded в котором я не разбираюсь.
embedded это отлично, но потеснить там C так же будет сложно
Все-таки трейсы нужны только если приложение падает, для отладки. При обработке обычных ошибок, которые приложение корректно отрабатывает согласно плану, а это 99.9999% ошибок (по примерной статистике с некоторых приложений с миллионами пользователей которые я знаю), трейсы не нужны. И как раз хотелось бы избавиться от громоздких конструкций типа трай кач, потому что корректная обработка ошибок очень важна, а когда она излишне многословна то заставить разработчиков делать все правильно бывает слишком сложно.
Я, кстати, добрался до google, и, например, для python такая возможность есть.
Но, говоря, откровенно, проблема в том, что далеко не всегда можно точно сказать, traceback в каком месте и где вам не будет никогда нужен, а такие ошибки потом могут оказаться довольно дорогими.
Например, у вас где-то отваливается коннект, вы знаете, что в конкретном месте, но вместо traceback у вас в логах просто куча сообщений вида "socket connection reset:49". Такие ситуации обычно куда хуже, чем небольшое излишество от traceback.
У меня тут непопулярная точка зрения, но у меня во многих проектах вместо трейсбеков стоят просто в начале строки в логгере путь к файлу и номер линии:
- [timestamp] path/file.go:42: Failed to connect to server: connection reset
и этого не просто с головой хватает, но еще и красиво, удобно и очень ускоряет процесс использования логов в дебаг-сессиях.
Трейсбек тут понадобится только тогда, когда структура программы станет запутанной и страшной, и ошибка будет исходит из неверных параметров, и родительских функций может быть 20 разных вариантов и тд. Тогда да. Но пока программы не гигантские и не спагетти — file:num хватает с головой.
И какой-же там оверхед? :) Одна CMP инструкция?
Как минимум копирование error, которое по факту может быть большим. Вот вам
package main
import (
"testing"
)
type errStr struct {
message string
}
type errStruct struct {
payload [4096]byte
message string
}
func (e errStr) Error() string {
return e.message
}
func (e *errStruct) Error() string {
return e.message
}
type FailFn func() error
func DoSomething(k int, ff FailFn) error {
if k == 0 {
return ff()
}
return DoSomething(k-1, ff)
}
func RunBenchmarkFib(b *testing.B, k int, ff FailFn) {
errn := 0
for n := 0; n < b.N; n++ {
err := DoSomething(k, ff)
if err != nil {
errn++
}
}
}
func BenchmarkStrErr(b *testing.B) {
RunBenchmarkFib(b, 10, func() error { return errStr{message: "fail"} })
}
func BenchmarkBigErr(b *testing.B) {
RunBenchmarkFib(b, 10, func() error { return &errStruct{message: "fail"} })
}
func BenchmarkNilErr(b *testing.B) {
RunBenchmarkFib(b, 10, func() error { return nil })
}
А вот результаты
/t/goben $ go test -bench=.
BenchmarkStrErr-12 20000000 115 ns/op
BenchmarkBigErr-12 1000000 1436 ns/op
BenchmarkNilErr-12 50000000 27.5 ns/op
PASS
ok _/tmp/goben 5.962s
Ну вот это другое дело — с бенчмарками интересно общаться.
Только бенчмарк у вас а) переусложнен (зачем рекурсия и фибоначчи для бенчмарка возврата ошибки?) б) бенчмаркает не только возврат ошибки, но и аллокацию ее. Отсюда и неверное трактование результата.
Дело в том, что интерфейс устроен так — это, по сути, структура, которая в себе держит два поинтера. Я вот тут про это писал — https://habrahabr.ru/post/325468/#interfeysy
Когда вы копируете интерфейсный тип error, копируются только собственно эти два поинтера — а то, куда они указывают, остается на своих местах в heap-е (что логично). Поэтому ваш бенчмарк сразу подозрительным показался.
Вот измененный бенчмарк, который трекает только возврат ошибки и ничего более. Кстати, это довольно частый паттерн, когда ошибка определяется один раз, например типа такого — var ErrNotFound = errors.New("not found")
— и дальше возвращается где нужно. Тоесть пример не надуманный, если что.
https://play.golang.org/p/WSiHxIU-o2
Результаты:
$ go test -bench .
BenchmarkString-4 2000000000 1.95 ns/op
BenchmarkStruct-4 1000000000 1.93 ns/op
BenchmarkNil-4 1000000000 2.38 ns/op
PASS
ok test/habr 8.882s
Да, вы правы — виновата аллокация ошибки.
Может тогда, как знающий человек, поясните результаты такого бенчмарка
https://play.golang.org/p/WM1wgiUb1y
Там мы вычисляем чисола фиббоначи (10е и 30е). Иногда падаем с ошибкой (всегда, никогда, в 1% и 20% случаев).
Результаты:
~/w/goben $ go test -bench=. Benchmark/err___small_k_without_errors-12 3000000 465 ns/op Benchmark/panic_small_k_without_error-12 3000000 426 ns/op Benchmark/err___big_k_without_errors-12 200 6801365 ns/op Benchmark/panic_big_k_without_error-12 300 5701481 ns/op Benchmark/err___small_k,_rare_errors-12 3000000 455 ns/op Benchmark/panic_small_k,_rare_error-12 3000000 430 ns/op Benchmark/err___big_k,_rare_errors-12 200 6833296 ns/op Benchmark/panic_big_k,_rare_error-12 300 5713887 ns/op Benchmark/err___small_k,_often_errors-12 3000000 463 ns/op Benchmark/panic_small_k,_often_error-12 3000000 437 ns/op Benchmark/err___big_k,_often_errors-12 200 6795496 ns/op Benchmark/panic_big_k,_often_error-12 300 5704373 ns/op PASS
Неужели panic
быстрее проверок if err != nil
? Или в бенчмарк опять закралась ошибка?
Собственно, в вашем бенчмарке ни возврат ошибки, ни паника ни разу не происходят. Я бы рекомендовал упростить бенчмарк до двух простых функций — вовзрат ошибки и перехват паники (опять же, тут мы бенчмаркамем не только panic(), но и recover()).
В вашем примере вроде как мы хотим сравнить error return vs panic, а бенчмаркаем вообще всё.Ну так мне интересно узнать общую разницу в производительности, а не просто единичный return и единичный panic. Приблизить ситацию к реальности, так сказать.
Собственно, в вашем бенчмарке ни возврат ошибки, ни паника ни разу не происходят.Виноват, вот исправленные результаты
play.golang.org/p/yppTDM0w2j
Benchmark/err___small_k_without_errors-12 3000000 492 ns/op
Benchmark/panic_small_k_without_error-12 3000000 437 ns/op
Benchmark/err___big_k_without_errors-12 200 7175016 ns/op
Benchmark/panic_big_k_without_error-12 200 5873139 ns/op
Benchmark/err___small_k,_rare_errors-12 3000000 476 ns/op
Benchmark/panic_small_k,_rare_error-12 3000000 482 ns/op
Benchmark/err___big_k,_rare_errors-12 200 7134222 ns/op
Benchmark/panic_big_k,_rare_error-12 300 5766522 ns/op
Benchmark/err___small_k,_often_errors-12 5000000 391 ns/op
Benchmark/panic_small_k,_often_error-12 5000000 371 ns/op
Benchmark/err___big_k,_often_errors-12 300 5732042 ns/op
Benchmark/panic_big_k,_often_error-12 300 4775898 ns/op
Benchmark/err___small_k,_always_errors-12 50000000 28.9 ns/op
Benchmark/panic_small_k,_always_error-12 20000000 106 ns/op
Benchmark/err___big_k,_always_errors-12 20000000 62.8 ns/op
Benchmark/panic_big_k,_always_error-12 10000000 124 ns/op
Я бы рекомендовал упростить бенчмарк до двух простых функций — вовзрат ошибки и перехват паники (опять же, тут мы бенчмаркамем не только panic(), но и recover()).Ну вы же понимаете, что количество вызовов `recover` будет куда меньше, чем проверок `if err != nil`. В случае с проверками их нужно вставлять во все функции (даже если у нас 10 функций рекурсивно вызывают друг друга). Если это panic/exception — recover вставляется только в верхнюю (где и просиходит обработка), а `if err != nil { return err }` уже не нужна. Ясно что в случае неглубокого стека `if err != nil` выигрывает. Но судя по бенчмарку `panic` быстрее уже при глубине рекурсии 10, а это всего 108 рекурсивных вызовов, в реальных приложения может быть куда больше. При глубине 8-9 наступает паритет (это примерно 30 рекурсивных вызовов).
BenchmarkStruct-4 1000000000 1.93 ns/op
BenchmarkNil-4 1000000000 2.38 ns/op
Интересно, а почему результат с `nil` на 20% медленнее?
Как-то совсем не ясно, что же тут происходит и какой оверхед от передачи ошибки. Неужели нет шанса это узнать, кроме как изучить внутренности устройства компилятора Go?
Ну почему же. добавив флаг cpuprofile и memprofile можно попрофайлить и разобраться.
Мне кажется, в случае с nil там какая-то внутренняя магия по заворачиванию nil в интерфейс error
? Если определить nil-ошибку заранее, как в примере со struct, то выравнивается:
https://play.golang.org/p/rHSgVR4zDs
$ go test -bench . -benchmem .
BenchmarkString-4 2000000000 2.00 ns/op 0 B/op 0 allocs/op
BenchmarkStruct-4 1000000000 1.97 ns/op 0 B/op 0 allocs/op
BenchmarkNil-4 1000000000 2.27 ns/op 0 B/op 0 allocs/op
BenchmarkNilAllocated-4 1000000000 1.93 ns/op 0 B/op 0 allocs/op
PASS
ok test/habr 11.032s
Мне кажется, в случае с nil там какая-то внутренняя магия по заворачиванию nil в интерфейс error?Т.е. вы не знаете ответа и не может это объяснить?
Я не автор статьи (и не переводчик), но всё-таки хочу заметить, что ваши рассуждения не универсальны. Оверхед блока if виден невооружённым взгядом — вы в общем-то можете заранее сказать, какого вида машинный код в этом месте будет сгенерирован и как будут возвращаться результаты. В случае с исключением всё уже сложнее, вам придётся его именно что изучать. (Да, по поводу того, какого рода ошибка — в Go вроде бы можно именовать возвращаемые значения.) По памяти оверхед тоже важен — в embedded, например (реализация исключений в gcc использует чудовищный размер стека при его раскрутке, и этот размер вам приходится давать каждой задаче, если у вас RTOS).
Ну и в целом исключения — это дополнительная когнитивная нагрузка. Нет, я признаю их пользу, и использую их где могу, но просто это ошибка — думать, что они бесплатны со всех точек зрения. Когда вы пронизываете свой код множеством неявных нелокальных return, вы за это, естественно, расплачиваетесь повышенной когнитивной нагрузкой, повышенным риском ошибок.
Когда вы пронизываете свой код множеством неявных нелокальных return, вы за это, естественно, расплачиваетесь повышенной когнитивной нагрузкой, повышенным риском ошибок.
Я не совсем понимаю, чем явный return лучше, чем неявный, учитывая. что если я работаю с системой, я точно знаю, где как и когда ошибка будет перехвачена. Мне нет необходимости следить за тем, передал я ошибку дальше или нет, а эта функция возвращает ошибку и так далее. Я просто пишу код.
Мне кажется, что вот такой подход к ошибкам дает как раз больше когнитивной нагрузки, потому что теперь нужно еще следить за пробросом ошибок.
Но ведь не вы же один будете работать с системой. Ваш код будут читать другие люди, кто-то будет матёрым разработчиком, который знает этот код, а кто-то новичком, который никогда с вашей системой не работал. Ему-то откуда знать, где и когда ошибка будет перехвачена.
Я вот апокрифичный пример из C++ Annotations вставлю:
void fun() {
X x;
cout << x;
X *xp = new X{ x };
cout << (x + *xp);
delete xp;
}
Вот представьте, что вы читаете этот код в первый раз. Во скольких местах эта функция может вернуть, помимо подразумеваемого return в конце?
В тринадцати. Вот задумайтесь — тринадцать нелокальных невидимых goto в функции из пяти строк. Если вы выделили в функции какие то ресурсы и/или хотите поддерживать какие-то exception guarantees, вы должны их видеть. Везде. Исключения — отличная штука, но начинающим они бьют по мозгам; чтобы использовать их безопасно, нужен определённый стиль мышления, сам по себе он не вырабатывается, нужны практика и время.
А теперь представьте, как это выглядело бы с другим подходом. 5 строчек существенного кода и 39(!) строчек вида if err!=nil {}
.
Мне кажется, оба варианта плохи, тут скорее код нужно исправлять.
и 39(!) строчек вида if err!=nil {}.
Мне кажется, вы не поняли, почему в примере выше взялось 13 мест, и просто умножили 13 на 3, чтобы подпитать свой confirmation bias и самого же себя убедить в том, что вы правы. :)
Да, вы правы.
Давайте умножим 5 на 3 и получилим — 15 строчек. 75% всего кода — это шаблонный код, который не несет смысловой нагрузки.
Стала ли ситуация лучше?
Или я неправильно понял, и не каждая строчка этого кода может выбросить исключение?
который не несет смысловой нагрузки.
День, когда вы начнете видеть смысловую нагрузку в обработке ошибок будет самым важным днём вашей карьеры.
Вы подменяете понятия. Я не вижу дополнительной смысловой нагрузки в эмулировании проброса ошибок в 99% процентов случаев ради 1% случаев, когда это реально важно. Нет никакого реального профита, исключая вопросы перформанса, вставлять
if err != nil {
return nil, err
}
Это не делает ваш код чище, это не заставляет вас прорабатывать исключительные случаи старательнее.
Более того, как отметили ниже, вопрос перформанса тоже может быть спорный, потому что в какой-то момент количество if проверок станет настолько велико, что по замедлению производительности побьет плату, которую мы несем за возможность raise
в python, хотя вряд ли ее можно оценить отдельно от всего runtime в python.
Нет никакого реального профита, исключая вопросы перформанса, вставлять
Вам же уже 100 раз написали, что главный профит в том, что следующий программист, читающий код, мгновенно будет понимать, что происходит в случае ошибки, со всеми вытекающими.
Уже 101 раз.
Но ведь это не так. Когда я пишу raise Exception
я всегда понимаю, что эта ошибка или будет обработана выше (а если таких мест много, то нужно ставить вопрос о рефакторинге) или обработается фрейморков и выдаст сообщение про ошибку, или уронит программу, если его нет. Только три случая.
В случае if-блока я только понимаю, что происходит с ошибкой локально. Буквально ничто не гарантирует, что эта ошибка будет как-то полезно обработана сверху, для этого мне нужно каждый раз смотреть весь стек вызова и убеждаться в том, что ошибка будет обработана как положено.
Это будет работать так, как вы описываете только в том случае, если ошибка обрабатывается прямо тут. Но для таких случаев обычно в других языках программирования используют локальные try-catch блоки с одной или двумя инструкциями, которые приводят к тому же самому результату.
Я потому и спросил про микросервисы, потому что этот ваш взгляд подходит маленьким проектам с маленькими функциями и маленьким стеком вызова. А как только он растет, начинаются все вот эти проблемы, к которым я аппелирию.
Я вот возьмите эту строчку и скажите, что произойдет с ошибкой, которую тут вернули. Без знания стека вызова, пожалуйста.
Вы не понимаете и оперируете какими-то концепциями в вакууме. Эта строчка вам даёт самое важное:
- ошибка в этом месте будет обработана
- эта ошибка останавливает дальнейшую работу функции
- ошибка передается наверх как есть
Этого уже более чем предостаточно чтобы понимать функцию, с которой вы работаете. Если вы хотите понять, как она обрабатывается выше — вы просто идёте выше и смотрите там. Но поинт в том, что всё это явно и ментальная модель программы строится на лету и быстро.
Я пойду писать бот, который будет автоматически на ваши комментарии отвечать одним и тем же :)
Мне кажется, у вас процедурная деформация. Вы работаете не с функцией, вы работаете с программой, которая являет собой сложную композицию функций. И понимания работы отдельных функций совершенно не достаточно для понимания работы программы (если бы все было так просто, то этого бы не существовало). Отдельные функции никогда не дадут вам понимая работы программы.
Эта строчка не дает вам самого важного. Понимания того, как именно программа поведет себя, когда вы вернете ошибку. А raise Exception
дает.
А если какой-то парень забыл, что ваша функция может вернуть ошибку и не добавил обработку? Или добавил, но забыл ее прокинуть дальше?
у вас процедурная деформация
Понятно.
А если какой-то парень забыл
Пусть девушка значит пишет.
у меня собралось, и никаких усилий я не приложил.
А '_' сам себя написал, да? :) Вы фанат своего мнения, я смотрю.
Я сделаю код реальнее, что бы вы мне поверили.
Я с вами тут общаюсь исключительно ради повышения количества комментариев к статье, ибо уже за второй год общения с вами в комментариях тут понял, что вы не восприимчивы к аргументам и безуспешно пишете из года в год тонны комментариев с одним и тем же. Хоть монетизирую эту вашу особенность :)
В вашем примере вы не просто явно замьютили ошибку, но еще еще и комментарий добавили. Лучшего примера того, что в Go ошибку игнорировать нужно только сознательно потрудившись, нельзя было найти. Браво.
В вашем примере вы не просто явно замьютили ошибку, но еще еще и комментарий добавили. Лучшего примера того, что в Go ошибку игнорировать нужно только сознательно потрудившись, нельзя было найти. Браво.
Но ведь я же не один программирую. Вот я написал такой код в функции, которая находится где-то в середине вызова, а потом какой-то другой программист такой будет две недели искать причину, почему его ошибка не пробрасывается, потому что удивительно строгий, но такой же удивительно непоследовательный компилятор golang это спокойно схавал.
Хотя мне кажется, что такой mute ошибки куда серьезнее, чем неиспользуемый импорт.
потому что удивительно строгий, но такой же удивительно непоследовательный компилятор golang это спокойно схавал.
Да вам ток шоу надо режиссировать :) Вы компилятору явно приказали проигнорировать ошибку, еще и коммент для программиста оставили. С чего бы компилятору это не схавать? Ну вы тролль профессиональный, да уж.
Подскажите, как компилятору помочь игнорировать неиспользуемый импорт?
Я просто не понимаю, почему одни вещи, которые вроде как проверять критичные ошибки, всегда работают, а другие — нет. Или дополнительные пару мегабайт веса приложения гораздо важнее потерянной ошибки? И я понял бы, если бы компилятор не знал, что это ошибка. Но он же знает.
Подскажите, как компилятору помочь игнорировать неиспользуемый импорт?
Просто перестаньте писать бессмысленные комментарии на Хабре и сразу начнёт игнорировать. Это же в спецификации Go написано.
Ни в коем случае, не умаляя преимуществ Go в сфере применения системных утилит, но...
В Go можно написать что-то похожее на
func doSmth() {
db.Exec("important select")
}
и никто никогда никак не узнает, где произошла ошибка.
Вышеописанный случай может произойти, к примеру, при смене минорной/патч версии какой-либо зависимости.
Кто-то может написать в таком виде рабочий вариант, только личь чтобы проверить концепт и забыть отрефакторить до продакшн-реди кода.
Исключения позволяют узнать о том, что кто-то проигнорировал ошибку и обработать её адекватным образом.
Раз в треде говорят о JS, то в ноде поведение по-умолчанию — кричать в error log, что кто-то не обработал ошибку (если говорим о промисах). В будущем — будет падать. Отлавливать проигнорированные ошибки тоже просто — process.on('error', cb);
В других языках нередко оборачивают пользовательский код в один большой try {} catch () {}
Кто-то может написать в таком виде рабочий вариант, только личь чтобы проверить концепт и забыть отрефакторить до продакшн-реди кода.
Именно с этим Go и борется. "Проверить концепт и потом добавить проверку ошибок" это стандартная модель в языках с эксепшенами (ну, на моём опыте). Сам подход это стимулирует — "ошибка это крайний случай, пока что не важно, мне только проверить, потом добавляю нормальные обработчики и тд". В Go как мантра вбивается (особенно при объяснении, как это у вас тут нет эксепшенов) что ошибки — это такие же значения. Вы не вызываете функцию, которая возвращает значение, чтобы "потом в продакшн коде с этим значением что-то сделать". Нет, если вы ее вызвали — вы как-то реагируете на то, что она вернула — так же и с ошибками в Go.
Поэтому то что вы написали, чисто теоретически в вакууме возможно, но на самом деле нет, так никто не пишет, а если какой совсем новичок (пришедший из языков с эксепшенами) и пишет, то ему/ей быстро вправляют мозги и бьют по рукам коллеги, код ревью или линтер errcheck.
Исключения позволяют узнать о том, что кто-то проигнорировал ошибку
Это в продакшене уже получится узнать только :)
Поэтому то что вы написали, чисто теоретически в вакууме возможно, но на самом деле нет, так никто не пишет, а если какой совсем новичок (пришедший из языков с эксепшенами) и пишет, то ему/ей быстро вправляют мозги и бьют по рукам коллеги, код ревью или линтер errcheck.
Можно подумать, в других языках не так же?
Это в продакшене уже получится узнать только :)
Разве что у вас не какой-то язык с жестким требованиям для отлова ошибок, например, java.
Я вам по секрету скажу: это и называется "раскрутка стека". И чтобы код этой типичной раскрутки не мешал воспринимать алгоритм и не было риска случайно проигнорировать ошибку, в языках высокого уровня в своё время и появилась идиома исключений, предпалагающая раскрутку стека с последующим падением как разумное, естественное и практичное поведение по умолчанию.
Вы меня превратно поняли; я не говорю, что исключения надо повсеместно похоронить и заменить тонной boilerplate. :) Я просто привёл пример того, что исключения — это не волшебное средство от сложности и "просто писать код" всё равно не получится. Они просто переносят сложность в другую плоскость — пишут за вас невидимые if (err != nil) { return err }. В каких-то задачах это приемлемо и желательно, в каких-то совсем наоборот, но я не знаю способа решить раз и навсегда, какие из задач какие. Думаю, что никто не знает, иначе бы мы тут не спорили.
Я плохо знаю C++, так что можете, пожалуйста, для меня подтвердить — там большинство ошибок же будет связано с невозможностью выделить или освободить память, а так же с отсутствием stdout?
stdout, нет, такого нет… Управление памятью — да, добавляет рисков, но на мой взгляд это не самая большая их доля, плюс современный идиоматичный код с умными указателями этому не так подвержен. Посмотрите внизу, где мы с vintage дискутируем — там пример неприятностей, которые не связаны с управлением ресурсами. Исключение может застать вас в процессе изменения данных, которые вы должны изменить консистентно (или не изменить вообще, если происходит ошибка).
void doMyJob() {
auto file = fopen( ... );
scope( exit ) file.close();
// работаем с файлом и не боимся исключений
}
Или даже так:
void doMyJob() {
scoped file = File( ... );
// работаем с файлом и не боимся исключений ведь файл будет закрыт при выходе из скоупа
}
Или вообще так:
void doMyJob() {
auto file = File( ... );
// работаем с файлом и не боимся исключений ведь файл будет закрыт, когда на него не останется ссылок
}
Да, RAII — замечательная штука, не спорю. Но управление ресурсами — это далеко не единственное место, где исключения привносят сложность. Давайте возьмём ваш пример и добавим работу с файлом:
void doMyJob() {
auto file = File(...);
// работаем с файлом и не боимся исключений ведь файл будет закрыт, когда на него не останется ссылок
auto data = readSomeData(file);
writeRecordHeader(file);
auto record = processData(data); // Бросает исключение.
writeRecordBody(file, record);
}
Упс. Ваш файл был корректно закрыт, с этим всё в порядке, вот только функция его оставила в повреждённом состоянии — заголовок добавила, а тело нет, и в результате там мусор. И всё потому, что функцию processData() писали не вы, и кто знал, что она бросает исключение.
Пример с файлом утрированный и частный, поэтому давайте абстрактнее:
void UpdateRecord(Record* r, Data d) {
r->x = CalculateNewX(d);
r->y = CalculateNewY(d);
}
Если CalculateNewY() бросит исключение, то запись останется обновлённой лишь наполовину. И надо учитывать всех, кто бросает исключение. Его может бросить сама функция, его может бросить конструктор копирования или оператор преобразования типа (если CalculateNewY() возвращает не тот что нам надо). Или оператор присваивания для поля y, скажем.
Ресурсы — это не самая большая проблема с исключениями; RAII поможет освободить память или закрыть файл. Проблема в том, что исключения — это невидимые глазу if (...) { return; } в вашем коде, и если вы их не учитываете, то запросто можете оставить мир в некоем фатально видоизменённом состоянии после ошибки.
Но управление ресурсами — это далеко не единственное место, где исключения привносят сложность.
Нет там никакой сложности в современном языке.
И всё потому, что функцию processData() писали не вы, и кто знал, что она бросает исключение.
В языке с исключениями все функции их могут бросать. Это аксиома. В любом случае ваш код должен выглядеть так:
void doMyJob() {
auto file = File(...);
auto data = readSomeData(file);
auto record = processData(data); // Бросает исключение или взрывает компьютер.
auto newFile = tmpfile();
newFile.writeRecordHeader();
newFile.writeRecordBody(record);
file.remove();
newFile.rename( file.name );
}
Пример с файлом утрированный и частный, поэтому давайте абстрактнее:
Ваш код при многопоточном доступе точно так же огребёт проблем. Кроме того, как вы будете восстанавливать консистентность, если уже стёрли x?
исключения — это невидимые глазу if (...) { return; } в вашем коде
Нет там никаких if-return. Прелесть исключений в том, что вы за них платите лишь при установке обработчика и раскрутке стека. А вот ваши ветвления больно бьют по процессорному конвееру всегда.
Нет там никакой сложности в современном языке.
Возможно. Но вокруг тонны legacy кода, не готового к RAII, и с ним приходится как-то жить.
В языке с исключениями все функции их могут бросать. Это аксиома. В любом случае ваш код должен выглядеть так:
Именно. :) Выше я писал о том, что исключения предполагают определённый стиль мышления, и вы избавили меня от необходимости это иллюстрировать. Да, в языке с исключениями все функции под подозрением, и это вырабатывает у вас навык писать в таком стиле — группировать работу функций в собственно работу, и commit фазу, где закрепляются результаты этой работы. Это как раз и есть способ обеспечить strong exception guarantee. Но сам по себе этот навык не вырабатывается, это и есть та сложность и та когнитивная нагрузка, которую несут исключения.
Ваш код при многопоточном доступе точно так же огребёт проблем. Кроме того, как вы будете восстанавливать консистентность, если уже стёрли x?
При чём здесь многопоточность? В этой функции нет глобального состояния. (Если что, запись у каждого потока своя, мы это не видим, да и не про то речь.) А консистентность — вы ведь уже сами продемонстрировали выше, в коде. Консистентность приходится не восстанавливать, а просто не нарушать — строить функцию так, что сначала идёт работа, а потом commit phase, где результаты работы (причём, если нужно strong exception guarantee, то там всё должно быть nothrow) присваиваются x. Конечно, то же самое вы будете проделывать с кодами ошибок, но в том-то вся и суть, что в отсутствие исключений вы видите явно, какая функция может упасть, а вот с исключениями они все под подозрением, и вам приходится программировать защитно, вот в таком стиле.
Нет там никаких if-return. Прелесть исключений в том, что вы за них платите лишь при установке обработчика и раскрутке стека.
Оставляя в сторону ту самую когнитивную нагрузку, которую вы платите за кучу скрытых нелокальных goto в коде… Да, нагрузка времени исполнения выше у if'ов. Но откуда мнение, что это везде настолько критично? Плюс я уже приводил выше пример — за исключения я плачу ещё и памятью во время раскрутки стека. В embedded, если вы используете RTOS, это здорово портит жизнь и (в моей практике) заставляет (именно в embedded) отказаться от исключений — при раскрутке одномоментно используется очень много стека, и стек каждой задачи должен быть достаточно большим, чтобы быть к этому готовым, а задач может быть много. Прелести исключений не универсальны, и за них приходится платить.
это вырабатывает у вас навык писать в таком стиле — группировать работу функций в собственно работу, и commit фазу, где закрепляются результаты этой работы
Как я, надеюсь, показал это хороший навык, независимо от поддержки в языке исключений.
В этой функции нет глобального состояния.
Есть внешнее состояние, которое может оказаться и глобальным.
Если что, запись у каждого потока своя, мы это не видим, да и не про то речь.
В Go уже завезли Thread Local Storage? Впрочем, если CalculateNewX снимет горутину с ядра, то следующая горутина увидит запись в неконсистентном состоянии.
в отсутствие исключений вы видите явно, какая функция может упасть
Вы мне покажите лучше функцию, которая гарантированно не может кинуть панику :-)
а вот с исключениями они все под подозрением, и вам приходится программировать защитно, вот в таком стиле.
Не приходится. Исключения ловятся пачкой на более высоком уровне. Нет смысла каждый вызов заворачивать в try-catch, как вы делаете в Go в соответствии со своей "определённой формой мышления".
за исключения я плачу ещё и памятью во время раскрутки стека
Ну это уже особенности конкретной кривой реализации исключений. В любом случае вполне нормально повышенное потребление ресурсов в исключительных ситуациях, а не постоянно.
В Go уже завезли Thread Local Storage? Впрочем, если CalculateNewX снимет горутину…
… которая гарантированно не может кинуть панику :-)
Извините если я у вас случайно создал впечатление, что мы обсуждаем Go. :) Мы обсуждаем исключения. Я на Go в общем и целом не пишу (так, потыкал для общего развития), поэтому не буду развивать тему паники (а чем она не исключение?) и TLS (что вам мешает просто не передавать эти данные во много горутин/потоков? если передаёте, то уж наверное это делаете осознанно и не каждую минуту). В общем, мы не про Go и уж точно не про shared state. Для простоты представьте что у нас один поток, проблема exception safety от этого никуда не денется.
Как я, надеюсь, показал это хороший навык, независимо от поддержки в языке исключений.
Не хочу вас расстраивать — но, нет, не показали. Вот кстати, вы сейчас клеймили if'ы за то, что они сбрасывают конвейер (я бы не согласился, кстати — во-первых есть предсказание ветвления, во-вторых ему ещё и помочь можно, указав наиболее вероятную ветвь). Но возьмите такой пример:
void Update(Record* r) {
GetNewDataX(r->x, ...); // x - это массив.
GetNewDataY(r->y, ...); // y - тоже.
}
Если у вас язык без исключений, то вы можете увидеть, что GetNewDataX() не возвращает кода ошибки (и исключений нет, поэтому неявно вернуть тоже не может) и, если она, конечно, реализована правильно и соблюдает контракт, то она — подразумевается — всегда успешна, там нет ничего, что может упасть. (Да, всегда есть залётный нейтрон или глючащее оборудование, но только не говорите, что вы и в расчёте на это код пишете.) Поэтому функцию использовать безопасно. Но вот представьте теперь, что у вас есть исключения, и, поскольку вы сами сказали, что все функции теперь под подозрением, то придётся переписать так:
void Update(Record* r) {
// Тут выделяем массив temp.
GetNewDataX(temp, ...);
GetNewDataY(r->y, ...);
Copy(r->x, temp); // nothrow
}
Представьте, что x и y — массивы размером в мегабайт каждый. Внезапно, необходимость учитывать возможность выброса исключений вынудила вас выделять мегабайт памяти и тратить время на его копирование. Вы только что спустили в трубу гораздо больше тактов процессора, чем вы сэкономили на if'ах. Функция GetNewDataX() на самом деле не бросает исключения, но поскольку все в списке подозреваемых, пришлось написать неэффективный код.
Не приходится. Исключения ловятся пачкой на более высоком уровне. Нет смысла каждый вызов заворачивать в try-catch, как вы делаете в Go в соответствии со своей "определённой формой мышления".
Вы меня, кажется, не поняли. Под защитным стилем я имел (что и написал) не то, где вы размещаете try-catch, а то, как вы организуете работу функции. Если у вас вылетело исключение при частично модифицированных данных, то это не имеет отношения к тому, что вы ловите и на каком уровне — вызывающая процедура получит неконсистентный результат. И поэтому (как вы сами и продемонстрировали) вы переписываете свой код определённым методом. (И ещё раз — извините, я не "делаю в Go", не переходите на личности.)
Ну это уже особенности конкретной кривой реализации исключений. В любом случае вполне нормально повышенное потребление ресурсов...
Извините, но количество глобусов в нашем — очень ограниченная штука, не говорите мне, что у вас есть выбор из десятков компиляторов на любой вкус. Ну и где, собственно, критерий, что важнее — очень высокий оверхед, но редко, или маленький, но постоянно? Я вот не думаю, что вам при оптимизации часто приходится спускаться на уровень ошибок предсказания ветвлений и промахов кэша — поправьте, если я ошибаюсь.
Для простоты представьте что у нас один поток, проблема exception safety от этого никуда не денется.
А теперь представьте, что вас не устроила производительность и вы решили распараллелить работу. Сколько времени вам потребуется на нахождение таких мест, где вы завязались на эксклюзивность исполнения?
во-первых есть предсказание ветвления, во-вторых ему ещё и помочь можно, указав наиболее вероятную ветвь
Который адекватно работает, когда ветвлений не очень много. А когда промахивается — десяток тактов в мусорное ведро.
GetNewDataX() не возвращает кода ошибки (и исключений нет, поэтому неявно вернуть тоже не может). Поэтому функцию использовать безопасно.
Что произойдёт, когда эта чудесная безопасная функция выйдет за пределы массива?
Извините, но количество глобусов в нашем — очень ограниченная штука, не говорите мне, что у вас есть выбор из десятков компиляторов на любой вкус
Специфическим условиям могут быть специфические решения. Не идиомы ту пинать надо.
Я вот не думаю, что вам при оптимизации часто приходится спускаться на уровень ошибок предсказания ветвлений и промахов кэша — поправьте, если я ошибаюсь.
За меня об этом думают разработчики компиляторов. Благодаря этому я могу не париться, вынося очередной функционал в отдельную функцию. Компилятор не то что условия, а даже собственно вызова может не вставить, если посчитает нужным.
А теперь представьте, что вас не устроила производительность и вы решили распараллелить работу. Сколько времени вам потребуется...
А вы всегда пишете thread-safe код, вне зависимости от? То есть вы всегда исходите из предположения, что ваши входные данные всегда может кто-то щупать из другого потока? Извините, но… мне очень сложно заставить себя в это поверить. Учтите, что в таком случае просто сделать работу а потом обновить результат недостаточно. Даже простое копирование может наложиться на доступ из другого потока. Вы каждый кусок данных окружаете защёлками, каждый флаг делаете атомарным? В яве, например, пожалели что сделали старые Vector и Hashtable synchronized, потому что оверхед большой, а потокобезопасность нужна редко. Вы же только что превозносили исключения за то, что они позволяют вам не платить за то, что не используете, а теперь сами говорите, что хотите заплатить за потокобезопасность (которая вам пока не нужна). Я удивляюсь.
Вообще, строго говоря, я не знаю, откуда тут вообще аргумент про многопоточность. Организация кода для exception safety не даёт потокобезопасность каким-то магическим образом, и наоборот — код может быть потокобезопасным, но не exception safe.
Который адекватно работает, когда ветвлений не очень много. А когда промахивается — десяток тактов в мусорное ведро.
Вы тратите сотни (или тысячи, если в куче) тактов на выделение и копирование памяти, когда вам надо сделать временное хранилище результата и обеспечить exception safety, и для вас это приемлемо, а промах предсказателя ветвлений — трагедия. Про защиту от конкурентного доступа я уже молчу.
Что произойдёт, когда эта чудесная безопасная функция выйдет за пределы массива?
Функция принимает типизированный массив фиксированного размера, и компилятор бы её просто не скомпилировал, если бы был выход за границы? (И да, я не просто так упомянул "если она правильно реализована". Вы можете вернуть мусор и при этом не выбросить исключение.) Я не думаю, что это относится к теме обсуждения.
Специфическим условиям могут быть специфические решения. Не идиомы ту пинать надо.
Мы как раз про то, что нет универсально полезной идиомы, за все приходится платить так или иначе, где-то цена приемлема, где-то нет. Вы упомянули, что я плачу только в момент раскрутки стека — я же сказал, что это не значит, что я не плачу вовсе, и это не всегда приемлемая цена.
За меня об этом думают разработчики компиляторов. <...> Компилятор не то что условия, а даже собственно вызова может не вставить, если посчитает нужным.
Тогда о чём говорить? Компилятор и ветвления умеет оптимизировать — вы же не проверяете, сколько ветвлений он выбрасывает, сколько переставляет, угадывая, какая из ветвей более вероятна, что ему profile-guided optimization нашёптывает. Вообще, мир, в котором ошибки предсказания ветвления были бы самым значимым фактором в оптимизации программ, был бы поистину прекрасен.
В общем, вы пока не сказали, почему организовывать свою функцию в фазы работы и в фазу фиксирования результата — это в любом случае хорошая идея. На потокобезопасность это, если что, не влияет, потокобезопасность обеспечивается другими вещами. Накладные расходы вызывает. Зачем эта когнитивная нагрузка там, где исключений нет — не знаю.
Функция принимает типизированный массив фиксированного размера, и компилятор бы её просто не скомпилировал, если бы был выход за границы?
Как вы предлагаете это (проверку) реализовать?
Передать в функцию не голый указатель, а что-то наподобие std::array<T, N>? Если вы попытаетесь передать массив другого размера, это будет уже другой тип.
Нет, я не говорю о том, что сама реализация потом не попытается записать что-то в память за пределами массива. Во всяком случае в C/C++ вам никто не помешает, но и вопрос "а что если эта функция сделает что-то страшное" тогда не имеет смысла. Тогда всё взорвётся. Или функция вернёт мусор и всё взорвётся потом. Или она вернёт мусор и всё продолжит как-то работать. Но она в любом случае не сможет вернуть ни код ошибки ни исключение, потому что она сама не знает, что в ней баг. В коде нам приходится исходить их того, что функция следует контракту.
Во-первых, вы можете упереться в ulimit. Во-вторых, массив может выделяться в куче, а в самом классе будет только указатель. (Да, std::array<> выделяет статически, но в общем случае вы этого не знаете.)
Как вы предлагаете это (проверку) реализовать?
Шаблоны в C++ умеют делать такие страшные вещи, если не ошибаюсь.
auto doMyJob( int[3] data ) {
return data[4]; // Compilation Error: array index 4 is out of bounds data[0 .. 3]
}
Я стараюсь вносить изменения атомарно — это хорошая практика из которой уже вытекает и "exception safety" и "thread-safety" и "debug-friendly". D по умолчанию хранит все данные в TLS, а чтобы расшарить данные между потоками — нужно сделать их синхронизированными, что включается добавлением всего одного ключевого слова. Данные я не копирую без нужны, ведь есть COW и move.
Пусть функция принимает динамический массив, не заставляйте меня придумывать более сложный пример. Вы можете гарантировать, что в её коде нет ошибки и она не вылезет за пределы памяти и не сольёт логин-пароль злоумышленнику?
Вообще, по теме могу предложить почитать: https://habrahabr.ru/post/280378/
Я не совсем вас понял, и я недостаточно хорошо знаю D, чтобы понять специфику. Вот мы исходим из того, что ваш аргумент — структуру — может щупать другой поток. Вы сначала сказали, что многопоточность может вкрасться в любой точке функции, и надо её делать thread safe, а теперь вы говорите что в D всё в TLS и никаких общих данных нет, если вы сами явно не скажете. И как атомарность изменений (в контексте защиты от конкурентного доступа) помогает с exception safety, если исключения поднимаются вашим же потоком, то есть никак не затрагивают многопоточность? В середине изменения synchronized объекта у вас точно так же может вылететь исключение. Тут вам нужна уже другая атомарность — не в плане защиты от одновременного доступа, а в транзакционном плане.
Ну и про функцию — а как вы вообще собираетесь учитывать ошибки разработчика? Мы сейчас говорим об исключениях, заметьте. Вы мне возразили — как же можно, функцию вызвали, кода ошибки она не возвращает, говорит что хорошая, но внутри баг и она делает страшные вещи. Какая в таком случае разница с исключением? Да, в функции баг, но она сама об этом не знает, и потому не вернёт ни исключение ни код ошибки. Вы думали, что сливание паролей злоумышленникам обязательно сопровождается исключениями? Она просто вылезет за пределы, и тогда может произойти всё что угодно, но сама функция об этом не догадается и не вставит ни throw ни if (...). Мы можем судить о функции только по контракту.
За ссылку спасибо — почитаю, хотя я D никогда не ковырял, да и в Go в общем-то не силён.
Вы сначала сказали, что многопоточность может вкрасться в любой точке функции, и надо её делать thread safe
Ну, это в C и Go так, если функция работает с внешней структурой.
И как атомарность изменений (в контексте защиты от конкурентного доступа) помогает с exception safety
Если возникло исключение, то до атомарного изменения (одного машинного слова, например, ссылки на данные) дело не дойдёт.
Тут вам нужна уже другая атомарность — не в плане защиты от одновременного доступа, а в транзакционном плане.
Так она только такая и есть. Атомарное изменение для любого наблюдателя происходит либо целиком, либо не происходит вовсе. Если мы будем временно переводить объект в неконсистентное состояние, то мы теряем возможность восстановления в случае паники.
Ну и про функцию — а как вы вообще собираетесь учитывать ошибки разработчика?
В том-то и дело, что никак. Именно поэтому вы не сможете написать такую функцию, которая гарантированно никогда не паникует. Исключения дают единый механизм реакции на исключительные ситуации. Будь то, пользовательские исключения или системные.
Она просто вылезет за пределы, и тогда может произойти всё что угодно
Например, прочитает данные по нулевому указателю и получит отлуп, который грохнет весь сервер из-за одной "надёжной" функции где-то в глубине зависимостей.
Ну, это в C и Go так, если функция работает с внешней структурой.
Если возникло исключение, то до атомарного изменения (одного машинного слова, например, ссылки на данные) дело не дойдёт.
То есть вы для любых входных данных делаете так, что изменение их происходит атомарно, инструкцией с барьером. Вопрос — как? Вот предположим, что вам передали объект. У него есть метод SetX()
и метод SetY()
. Или даже просто два разных поля, которые вам предстоит изменить. Вы не можете записать эти два поля атомарно (в значении слова "атомарно", которое вы используете, которое вас защищает и от исключений, и от других потоков). Какие ваши действия в таком случае?
Так она только такая и есть. Атомарное изменение для любого наблюдателя происходит либо целиком, либо не происходит вовсе.
Так в том-то всё и дело, что вы защищаетесь от двух очень разных вещей — от стороннего наблюдателя, и от неявного поведения своего собственного кода. Вот давайте грубо, гипотетически перепишем старый пример двумя способами:
void Update(Record* r) {
EnterCriticalSection();
GetNewDataX(r->x, ...); // x - это массив.
GetNewDataY(r->y, ...); // y - тоже.
LeaveCriticalSection();
}
Отлично, с точки зрения других потоков r
изменяется атомарно. Но этот код всё равно не exception safe, потому что GetNewDataY() может бросить исключение.
void Update(Record* r) {
// Тут выделяем массив temp.
GetNewDataX(temp, ...);
GetNewDataY(r->y, ...);
Copy(r->x, temp); // nothrow
}
Этот код даёт strong exception guarantee. Но он ни разу не защищён от изменения данных со стороны. Да даже невинная Copy() может наложиться на операцию записи другим потоком, и у вас будут смешанные данные.
Видите? Мы защищаемся от двух разных вещей двумя разными методами, это разная атомарность. (И кстати, первый вариант всё равно не даёт вам защиту от многопоточности, потому что некий поток изменит вашу запись после критической секции и результат работы функции просто потеряется. Какой физический смысл вообще у того, чтобы защищать r
от параллельного доступа в этой функции? Можете объяснить, зачем это делать?)
В том-то и дело, что никак. <...> Исключения дают единый механизм реакции на исключительные ситуации. Будь то, пользовательские исключения или системные.
Например, прочитает данные по нулевому указателю и получит отлуп, который грохнет весь сервер из-за одной "надёжной" функции где-то в глубине зависимостей.
Я не понимаю. При чём здесь исключения? Как поддержка исключений (сама по себе) магически даст вам возможность отловить выход за границы массива или доступ по нулевому указателю? Исключения должны где-то выбрасываться; коды ошибок — где-то возвращаться. В случае ошибки разработчика этих мест там нет. Вы, возможно, привыкли к языкам с bounds checking, но во-первых это совершенно ортогонально исключениям (вы в принципе можете иметь проверку границ и без исключений), а во-вторых это не панацея от багов.
Возьмите код на С++ — там есть исключения, но при этом функция, которую вы вызываете, может радостно пройтись по чужой памяти, получить доступ к нулевому указателю (или указателю с мусором), и при этом никаких исключений выброшено не будет. Вы или получите мусор в памяти, или программа упадёт (и, да, грохнет весь сервер), но исключения вам не будет. Вы можете посоветовать стать ёжиком и использовать язык с bounds checking, но и это не защитит вас от ошибок разработчика — во-первых они не сводятся к ошибкам памяти, а во-вторых, и в этих языках (возьмём Java для примера) вы не можете гарантировать, что охочий до оптимизации программист не сделал реализацию через JNI или не использовал Unsafe, и теперь функция может пройтись по любой памяти, а исключения вы всё равно не получите. Исключения — это всего лишь один из способов пробросить ошибку наверх, но у вас нет гарантии, что все ошибки будут сопровождаться исключениями. Они не дают вам защиты от багов. Язык — может некоторую защиту дать, но это ортогонально способу доставки сообщений о тех багах, которые он поймал.
То есть вы для любых входных данных делаете так, что изменение их происходит атомарно, инструкцией с барьером.
Барьеры-то тут при чём? Они лишь упорядочивают операции.
У него есть метод SetX() и метод SetY()
У него будет метод Update() или я просто создам новый объект с нужным состоянием.
Дальше мне уже совсем скучно мусолить эту тему. Всё, что я хотел, я уже сказал. Что вы пытаетесь доказать, повторяя одно и то же, я не понимаю.
У него будет метод Update() или я просто создам новый объект с нужным состоянием.
Это не ваш объект. Вы не можете его менять, он вам даден сверху. Или опять "мышки, станьте ёжиками"? А в методе Update() не надо будет заботиться об атомарности? Вообще, вы не ответили ни на один из моих вопросов по сути. Я так и не узнал, что вы в действительности хотели сказать. Притянули за уши многопоточность — но не смогли объяснить, зачем она вам нужна. Заявили, что накопление результатов работы и атомарное изменение выходных данных — всегда хорошая привычка, но на контраргументы не ответили. Заявили, что ветвления портят вам производительность — и тут же сказали, что вы в общем-то никогда и не измеряли, пусть, мол, разработчики компиляторов заботятся. То есть в общем-то ничего и не сказали.
Ну, я не прессую — бог с ним, останемся при своих. Спасибо за беседу; пардон если где-то горячился.
Явный return явно показывает, что и где из функции может возвращаться, и по сигнатуре функции видно, какие у неё ошибки.
Да. но он не дает гарантии того, что дальше ошибка будет обработана как надо. И go build почему-то тоже. И что бы в этом быть уверенным, надо подняться вверх по стеку вызова, который еще может быть не очень тривиален и в этом убедится.
без комментария не понятно, что произойдет при ошибке в thingA(), пока не проскроллишь глазами в самый низ (и это на микроскопическом примере уже не очень приятно)
Вероятнее всего, для вас это непонятно потому-что вы, возможно, не знакомы с async / await
API и промисами в JS. Если произойдет ошибка в асинхронной функции, она свалится в ближайший .catch()
или в unhandledRejection
Собственно сниппет
(async function () {
})()
.catch((err) => {
console.error(err);
process.exit(1);
});
достаточно распространен, и, если вы с ним хотя бы раз встречались, вам будет ясно куда скроллить.
try { } catch (err) { } конструкция гораздо более громоздка, голословна, и при этом менее ясна, чем if err := thingB(); err != nil {… }. 5 строк против 3, при нулевых бенефитах в данном случае, и еще и спрятанной магией передачи ошибки.
А если я не хочу вообще перехватывать эту ошибку, и другие на каждый чих, если я за fail-fast?
снова же, приходится бегать глазами от начала main до конца, чтобы понять, точно ли не упустил ничего из этого flow-а программы, раскиданным в нескольких местах.
См. пункт про сниппет. Вам не надо бегать глазами по main, в конце лишь выводится ошибка и завершается процесс, т.к. асинхронные функции не выбрасывают uncaughtException
и не завершают процесс самостоятельно, если их не перехватить вручную.
А если я не хочу вообще перехватывать эту ошибку, и другие на каждый чих, если я за fail-fast?
А если хотите?
Вам не надо бегать глазами по main, в конце лишь выводится ошибка и завершается процесс, т.к. асинхронные функции не выбрасывают uncaughtException и не завершают процесс самостоятельно, если их не перехватить вручную.
Поставьте себя на место программиста, читающего реальный код чуждого человека. Мы читаем сверху вниз слева направо. Пока он не проскроллит до финального блока catch, он не узнает, что там происходит. Так что нет, не засчитывается.
и, если вы с ним хотя бы раз встречались, вам будет ясно куда скроллить.
Встречался, и вот, вы сами подтвердили, что нужно скроллить. В Go же все ясно на месте, без необходимости отвлекаться по коду.
И я не шучу, это очень важный момент, когда работаешь с чужими кодовыми базами, которые не держишь в голове и не помнишь наизусть что и где выбрасывается и кем и как перехватывается. Локальность важна.
Почему бы вместо всей этой свистопляски не использовать аналог Either
? Наличие ошибки будет объявлено в сигнатуре функции, а дальше — хотите обрабатывайте, хотите дальше пробрасывайте. Все явно и чисто.
Потому, что для этого нужны дженерики, а авторы Го не осилили их реализовать, поэтому постановили, что "дженерики не нужны*".
* кроме пары стандартных.
Да печаль совсем:
Уж простите, я не евангелист всех остальных языков, так что текстом:
Go: сидит дитетка, лепит куличики из песка: "Да я какой хош небоскрёб так сделаю, нужно только побольше куличиков".
Любой другой язык: сидит инженер и делает эти сложные расчёты: "чтобы ветер не сдул используем гибкие конструкции, чтобы не обвалился перераспределяем нагрузки, чтобы почва не осела ставим на мощных сваях и тд"
Особенно понравилось про «любой другой язык». Прямо все хороши, кроме Go, да.
Ну… да.
Вы посмотрите, какие они либы открыли. Куличик для обработки ошибок, куличик для баз данных, куличик для простого кеширования, куличик для расчета хеша.
То есть взяли, и написали кучу надстроек вместо того, что бы сразу приступить к решению задачи. Хотя тот же divan0 утверждал, что все же в go есть, видимо, эти ребята из Dropbox не постигли дзен.
About a year ago, we decided to migrate our performance-critical backends from Python to Go to leverage better concurrency support and faster execution speed.
Дропбокс — яркий пример успешного бизнеса построенного на довольно простой в реализации идее. Вы посмотрите сколько конкурентов у дропбокса, а сколько у, например, SAP ERP.
Я с го вообще никак, поэтому сейчас меня это повергло в шок.
Другими словами, "то есть как нет дженериков" o_o
Ну вот так. Авторы заявили, что шаблонные типи усложняют код. Поэтому их в языке и нет.
Ну вот так. Авторы заявили, что шаблонные типи усложняют код. Поэтому их в языке и нет.
Вы, как всегда, не разобрались в теме и обманываете людей. Ваши 150 аналогичных бредовых комментариев здесь вредят другим людям. Зачем, спрашивается?
То есть вот этот человек не прав?
Он написал совершенно не то, что вы написали. Вам не надоело еще людям морочить голову?
Таким образом ввод generics сильно усложнит язык — как реализацию, так и использование.
Дженерики невероятно сложны как по своей семантике, так и по реализации
или не так понял? как надо было понять?
а) дженерики это концепт, а шаблонные типы — это реализация
б) в Go нет дженериков, потому что авторы Go не видят способа их реализовать без того, чтобы не усложнить язык (не только код)
в) авторы Go не отказывались от дженериков, наоборот только за, если все специалисты Хабра расскажут, как и правильно сделать чтобы потом запрос в гугл «language X generics sucks» при X=Golang не выигрывал конкурс Generics Sucks Contest.
Тема гораздо более широкая и интересная, чем это представляют последователи «в каждом языке должны быть темплейты»-секты.
Касательно шаблонов..
усложнение рантайма,
Нет.
сборщика мусора
Нет.
процесса компиляции
Не более, чем go/generate, который с введением шаблонов можно будет вообще выпилить в большинстве случаев использования.
когнитивной нагрузки на программиста
Она уменьшится, ибо ему придётся меньше писать и читать generic кода.
тулинга
Незначительно.
грамматики
Незначительно.
и много чего ещё
Чего ещё?
Касательно шаблонов.
Нет.
Нет
Незначительно.
Незначительно.
Спасибо за доступное объяснение и хорошую аргументацию, всегда приятно послушать мысли умных людей. Теперь понятно.
в Go есть дженерики (типонезависимые функции
а это не усложнение рантайма, сборщика мусора, процесса компиляции, когнитивной нагрузки на программиста, тулинга, грамматики и много чего ещё?
а это не усложнение рантайма, сборщика мусора, процесса компиляции, когнитивной нагрузки на программиста, тулинга, грамматики и много чего ещё?
Слово trade-off слышали? Или у вас всё бинарно через "нужен/не нужен"?
— классов
— эксепшенов
— наследования
— тернарного оператора
— многих способов сделать одно и то же
— алгебраических типов
— арифметики поинтеров
— ручного освобождения памяти
Многих повергает в шок, что удобство программирования не зависит напрямую от количества фич, и что можно писать без них всех отличный код.
Вы уж простите, но удобство программирования на статически типизированных языках в 2017 так или иначе упирается либо в наличие классов, интерфейсов и наследования, либо в алгебраические типы и типы высших порядков (HKT). Ни того, ни другого, ни, уж тем более, дженериков (а как можно жить писать переиспользуемый код без них, я вообще не представляю) и в помине нет. И что я вижу — так это стокгольмский синдром на лицо.
Я терпеть не могу эксепшены, и, может быть, в следующей жизни готов помириться с фантастическими if err != nil
по всему коду, но я вас прошу, не пытайтесь тут продать язык, в котором якобы "специально" отказались от нереально удобных вещей, использующихся десятилетиями, в угоду мнимому субъективному удобству и понижению порогу входа. Последнее вообще рассмешило, вон в пхп тоже низкий порог входа.
знаете сколько я такого наслушался за последние 4 года
Ну а я бы сделал выводы.
И это не гневные бравады, что вы. Гневные бравады обычно у тех, кто что-то пытается защитить. Я же ничего не защищаю, а просто указываю, что прогресс в области программирования достиг определенных вещей, выкидывать которые, прикрываясь аргументами "ой сложно" — глупо.
Про неучей я ничего не говорил, видимо вам уже мерещится. И я вам ничего не доказываю, ни что они неучи, ни что идиоты, это вы всё сами.
Почитайте про этот чудесный синдром — вас чего-то лишают, а вы считаете, что это хорошо.
Ну а я бы сделал выводы.
Ну я же не только подобных стенаний наслушался. Чем более опытен программист, тем больше он понимает компромиссы языков и не возводит в культ различные фишки PLT. То, что, судя по вашей риторике, для вас является неоспоримым абсолютом и "прогрессом", тысячи очень неглупых люди давным давно уже прошли, набили шишки, сделали выводы и написали книжки по теме. Им тоже было, что сказать.
прогресс в области программирования достиг определенных вещей, выкидывать которые, прикрываясь аргументами "ой сложно" — глупо.
Именно поэтому меня и бесят тут высказывания троллей, которые берут такой многогранный вопрос, упрощают его до фраз "авторы Go сказали 'ой сложно' и не стали делать дженерики", которые разжигают в других неадекватные реакции.
Ну и люди, которые считают, что в других языках дженерики бесплатно достаются, имеют одни преимущества и не превращают зачастую код в Г — это тоже, конечно, верх наивности.
Почитайте про этот чудесный синдром — вас чего-то лишают, а вы считаете, что это хорошо.
Ну вот вас лишили простоты и читабельности кода, а вы считаете, что это хорошо. Такие "параллели" можно на всё что угодно натянуть, так что увы.
Ну вот вас лишили простоты и читабельности кода, а вы считаете, что это хорошо. Такие "параллели" можно на всё что угодно натянуть, так что увы.
Что? Вы вообще когда-то видели другие языки помимо golang? Или для вас interface{} образец читабельности? Дженерики можно обвинить в чем угодно, но в ухудшении читабельности их обвинить никак нельзя. У меня такое чувство, что вы пришли из С99 и теперь рассказываете, как все в go отлично. А тем временем на дворе 2к17, однако.
Именно поэтому меня и бесят тут высказывания троллей, которые берут такой многогранный вопрос, упрощают его до фраз "авторы Go сказали 'ой сложно' и не стали делать дженерики", которые разжигают в других неадекватные реакции.
Потому что так и есть. Только вместо того, что бы сказать "ой сложно" они сказали "у нас не получилось придумать". Причем никто тут не утверждает, что дженерики написать легко, вон посмотрите на Java, которая от плохо продуманной реализации страдает уже сколько версий.
Но черт возьми, от этого не стоит писать "дженериков в golang нет, потому что они не нужны". Они объективно нужны, потому что открывается любой более менее большой проект на go, делается grep и открывается прекрасный мир interface{}. И то, что создатели golang не смогли в дженерики является единственной причиной того, что их в языке нет. Никто не говорит "ахаха, лалки не смогли", потому что задача в самом деле сложная. Но делать из этого религию в духе "дженерики не нужны", глупо.
Вы вообще когда-то видели другие языки помимо golang?
Да.
Или для вас interface{} образец читабельности?
interface{}
это empty interface, мы говорим про интерфейсы в целом. Вы сначала разберитесь в теме, а потом пишите свой 200-й бессмысленный комментарий.
Но делать из этого религию в духе "дженерики не нужны", глупо.
Религию эту придумали вы, и вы же её своими комментариями и популяризируете. Вы даже не хотите разобраться в теме, поэтому повторюсь:
- дженерики это не шаблоны
- дженерики в Go есть, кроме определенных видов
- этого набора хватает с головой, чтобы за 5 лет стать самым популярным языком для клауда и бекендов
- ваш фанатизм относительно дженериков преувеличен и напоминает уже религию.
я лично натерпелся проблем с темплейтами в С++, и я никогда не имел реальной проблемы, в которой бы отсутствие дженериков затрудняло написание эффективного кода, или, тем более, делало его невозможным.
И еще раз — если вы настолько свято верите, что без привычных вам дженериков жизни нет и быть не может и это святое и ежедневная необходимость — напишите Experience Report, объясните конкретную проблему, которую вы решали, и как отсутствие дженериков стал вам на пути. Объясните насколько это серьезная проблема, чтобы авторы Go вообще могли оценить, какое решение могло бы подойти лучше.
А вот эти религиозные 300 комментариев, каждый из который сквозит незнанием темы, это любой школьник может.
interface{} это empty interface, мы говорим про интерфейсы в целом. Вы сначала разберитесь в теме, а потом пишите свой 200-й бессмысленный комментарий.
А я думал вы знаете Go. interface{} это еще способ эмулирования дженериков, как это делает Java, но вручную. Как и большинство вещей в go.
дженерики в Go есть, кроме определенных видов
Ну да. Generic в go — это interface{}. И да, это конечно же отличный дизайн, как и все в go.
этого набора хватает с головой, чтобы за 5 лет стать самым популярным языком для клауда и бекендов
Потому что он заменил C? Ну да, это все знают.
я лично натерпелся проблем с темплейтами в С++, и я никогда не имел реальной проблемы, в которой бы отсутствие дженериков затрудняло написание эффективного кода, или, тем более, делало его невозможным.
Невозможного ничего нет, пока есть приведение к interface{} и обратно, но попробуйте написать какой-то универсальный контейнер, например, монаду Maybe. И поиспользовать ее на go.
interface{} это еще способ эмулирования дженериков,
Ну да. Generic в go — это interface{}
Я имел ввиду встроенные типонезависимые функции, но я понял вашу стратегию — написать так много бреда, чтобы любое следующее высказывание уже тонуло на фоне предыдущих и не выглядело таким же бредовым.
попробуйте написать какой-то универсальный контейнер
Попробуйте решить реальную задачу на реальном проекте. Go для них создан.
Docker решает? Там вот используются функции, которые принимают interface{}.
Или вам нужно еще десяток проектов накидать, что бы вы поняли, то, что дошло до создателей языка?
Вы вообще о чём? Я не слежу за вашими потерявшимися мыслями, которые вы гуглите перед каждым комментом, чтобы убедится, не сильно ли вы бред несете. Если вы продолжаете демонстировать чудеса бинарной логики и пытаетесь мне доказать, что использование пустого интерфейса автоматически означает провал и серьезные проблемы, то тщетно — это лишь ваша фантазия.
Ах, ну да. Вы не можете следить за стеком вызовов, я и забыл.
Вот такой у нас тут был pipeline:
- Дженерики не нужны
- Еще как нужны, просто вместо них используется ущербный вариант через interface{}
- Нет, это ничего не нужно и вообще без него все отлично
- Ну вот нет, напишите такую монаду
- Ох нет, это выдуманная проблема, а вот вы напишите реальный проект
- Docker реальный проект?
- И вот мы тут.
Попробуйте решить реальную задачу на реальном проекте. Go для них создан.
А можете привести пример такой реальной задачи на Go? Ну там бухгалтерия и склад какой-нибудь или АвтоКАДа? Может CRM, ERP? Игрушка полноценная? Что-то, что решает реальные задачи, а не служит инфраструктурой?
Разумеется, есть. Например, вот эта. В которой самая важная часть такого рода систем, а именно кастомизация, загнанна в интерфейс, что предвещает лишь страдания.
И еще одна, которая попытаеться повторить удачу odoo (раньше openerp), но с таким синтаксисом они далеко не уедут.
Ну там бухгалтерия и склад какой-нибудь или АвтоКАДа? Может CRM, ERP? Игрушка полноценная? Что-то, что решает реальные задачи, а не служит инфраструктурой?
Смотрите, есть различные ниши — например десктопное приложение это не тоже самое, что консольная утилита, а firmware для embedded устройства это не тоже самое, что массивная распределённая кластерная система. Вы сейчас пытаетесь сказать, что одни ниши — это "реальные проблемы", а другие ниши (сервера, в частности) — это "инфраструктура". Но это, понятное дело, очень неверный взгляд на вещи, я бы даже сказал, устаревший. Сам помню то время, когда основная масса писавшегося софта — это были десктопные программы и игрушки. Но мы же в 2017, в мире облаков, кластеров, распределенных систем, микросервисов и блокчейна. Новые языки и технологии больше ориентированы на эти ниши, а не на умирающие вроде десктопного софта. Ну а Go так и вообще прямым текстом был рождён из-за того, что существующие языки для этих новых ниш были слишком неповоротливыми и неоправданно усложнёнными.
Причём тут десктопы? Всё в онлайне и облаках. Но бизнес-логика от этого не меняется, если нужно начислять проценты по сложной схеме, то их нужно начислять, если нужно чтобы остаток стульев на складе был неотрицательным, то он должен быть неотрицательным, если урон от критов должен быть в 10 раз больше обычного, а криты должны быть статистически в 20 раз реже обычных, то так и должно быть.
Все примеры реальных проектов, что я знаю — инфраструктурные — сервера, демоны, утилиты и т. п., а, как говорится, делать-то что умеешь?
сервера, демоны, утилиты и т. п., а, как говорится, делать-то что умеешь?
Спасибо, это смешно.
А ответ будет? Что пишут на Гоу более эффективно чем на других языках? Сервисы учёта, сервисы документооборота, сервисы моделирования, игровые сервера, ну хоть какой-то пример есть сложных систем, моделирующих реальный человеческий мир? Да, это круто, если Гоу может эффективнее всех взять что-то из базы и отдать по хттп. А примеры когда он создаёт что-то, на основании сотен параметров и событий реального мира и кладёт в базу, есть?
Почти все реализации блокчейна пишут на Go, например (btcd, geth, hyperledger, etc). Это конечно, не так круто, как «программы бухучета» из вашего «реального мира», но «проценты там тоже можно начислять» :D
Ну вот как-то так получается, что из реальных проектов на Go на слуху только инфраструктурные. Блокчейны тоже по сути инфраструктурные с простейшим по своей сути механизмом контроля целостности. Там где предполагается включение в блокчейн произвольной логики используется какой-то DSL, а не Go.
Как по мне, то Go системный язык и в целом не полноценный даже, а с целевой нишей диспетчерезации потоков данных, оркестрацией и синхронизацией процессов и прочим жонглированием. Примеры обратного как-то не на слуху, как-то только системные задачи выходят на свет. Серьёзные, но не имеющие самостоятельной ценности, обеспечивающие запуск и взаимодействие процессов на других языках, которые реально и моделируют бизнес-процессы реальной экономики.
Ну и да.
этого набора хватает с головой, чтобы за 5 лет стать самым популярным языком для клауда и бекендов
Вам не стыдно так нагло врать то?) OpenStack вот почти и не слышал про go. Куча бекенда все еще на Java, C#, python и прочих.
все еще
Именно. Вы же знаете, какая инерционность тут. Люди десятилетиями не хотят слазить от "удобных им технологий", и готовы тратит тысячи часов, чтобы троллить другие языки, вместо того, чтобы повышать свои знания и делать полезную работу. Ну, конечно же знаете, вы же самый яркий пример тут.
Вам люди пишут про реальные проблемы языка — вы говорите, что это виноваты люди, что они не поняли go-way, что фича X не нужна, что они ничего не понимают.
Что бы слезть с удобных технологий нужна причина. Бенчмарки не решают для большинства проектов, решает инфрастуктура и полезность языка. И тут golang как бы где-то в середине. Те, кто переходят с него на C готовы его целовать, те, кому приходится переходить в более гибких языков на него не из-за проблем с производительностью только плюются и правильно делают. Потому что переход не решит никаких проблем, кроме проблем производительности и то, возможно. Потому что i/o bound задачи такой переход не пофиксит.
вы говорите, что это виноваты люди, что они не поняли go-way
Сами придумали, и сами меня в этом обвинили. Покажите, где я такое говорю.
кому приходится переходить в более гибких языков на него не из-за проблем с производительностью только плюются и правильно делают.
Зачем же переходят, если и языки более гибкие, и производительность не нужна, и инфраструктура и полезность куда лучше, чем Go, и дженерики, и эксепшены — рай же, зачем переходить?
Зачем же переходят, если и языки более гибкие, и производительность не нужна, и инфраструктура и полезность куда лучше, чем Go, и дженерики, и эксепшены — рай же, зачем переходить?
Ну так вы же форсите go, вы и отвечайте.
Сами придумали, и сами меня в этом обвинили. Покажите, где я такое говорю.
https://habrahabr.ru/post/337098/?reply_to=10402372#comment_10398648
Ну так вы же форсите go, вы и отвечайте.
Вы сами придумали фантазию, сами её описали, и теперь требуете мне её обосновывать. Как всегда уровень неадеквата в ваших комментариях зашкаливает.
Сами придумали, и сами меня в этом обвинили. Покажите, где я такое говорю.
https://habrahabr.ru/post/337098/?reply_to=10402372#comment_10398648
И где там слова "люди виноваты" и "go-way". Зачем вы каждый свой комментарий перекручиваете и пишете заведомо ложные вещи, которые порождают опровергающие комментарии, на которые вы отвечаете таким же образом?
Иногда проще, ну, не пользоваться языком.
Проще, чем что? Чем написать понятным языком о том, как вы решали реальную задачу, и отсутствие пользовательских дженериков привело к тому, что код стал неэффективным, нечитабельным, глючным или некорректным? Да ладно, если бы хотя бы 1% свято верящих в абсолют дженериков понимал вопрос, таких бы репортов было бы уже тысячи, понятно и доступно написанных, без этих надменных бравад о том, какие авторы Go недалекие. Но что-то пока ни один не осилил.
чтобы за меня для простых типов генерил всякие селекты-инсерты.
Генерьте на здоровье. Есть go generate
, встроенные библиотеки для работы с AST — такое не сложно написать и генерить себе что хотите для своих типов. Думаю, что если кому-то такое подход нужен был, то уже написано.
символьное дифференцирование на этапе компиляции.
Я не совсем понимаю о чём речь, но звучит как описание решения какой-то проблемы, а не описание проблемы.
некий алгоритм машинного обучения.
Очень конкретно, да.
4, 5, 6
Вы делаете ту же ошибку, что и комментатор выше — вы рассказываете не проблему, а уже готовый способ решать в вашем удобном языке. Я потому и приводил пример про вопрос "горутину в Хаскеле", потому что ваш пример "монадка для кодогенерации" это именно такой же нонсенс.
Неужели сложно так описывать проблему, если она реальна?
Ну, мы же всё-таки решаем проблемы, а не создаём их? :)
У вас в категорию реальных проблем входят только опердени и перекидывающиеся джсонами микросервисы, что ли?
Ну неужели такие вещи нужно объяснять. Это как прийти к водителю Теслы и спрашивать его, куда заливать бензин, вместо того, чтобы объяснить проблему "добраться из пункта А в пункта Б".
Вам приводят реальные примеры, вы говорите, что это нереальные примеры и вообще уже имеющися способы.
Вы вправду не видите разницу между "описать проблему" и "описать придуманное решение"? Будь вы продакт менеджером вы прямо так бы и писали в беклог — "сегодня мы должны написать монадку" и все такие — "да, да, пишем монадку, ведь наша бизнес модель построена на решении проблемы отсутствия монадок".
Ну нет. Там стоит задача "необходимо заставить разработчиков обрабатывать null-значения и не игнорировать ошибки, а то у нас каждый релиз минимум 10 багов на эту тему". Вот кроме монады Optional ничего для этого не придумали, нужна монада.
Изменять команду или как-то повлиять на индусов в разработке вы не можете.
Да нет там ничего сложного.
Парсим шаблон:
T sum( T )( T[] list ) {
T result;
foreach( item ; list ) result += item;
return result;
}
Видим использование:
sum!int([ 1 , 2 , 3 ]);
Подставляем тип в шаблон, генерируем код и компилируем:
int sum( int[] list ) {
int result;
foreach( item ; list ) result += item;
return result;
}
Видим другое использование:
sum!float([ 1.1 , 2.2 , 3.2 ]);
Подставляем тип в шаблон, генерируем код и компилируем:
float sum( float[] list ) {
float result;
foreach( item ; list ) result += item;
return result;
}
Уже даже таких простых шаблонов без выведения типов уже хватило бы, но нет, "это слишком сложно".
Создатель Node.js: «Для серверов я не могу представить другой язык кроме Go»