Pull to refresh

Comments 180

а как в Go заменяют ошибками стектрейс?
Ошибка и стектрейс — это разные понятия, их не нужно заменять.
Стектрейс это контекст, его можно получить через runtime.Stack или выбросив panic().
Есть еще stackerr от facebook. Говорят популярная библиотека.
Возможно, некоторые не понимают разницу между «предвиденной ошибкой» и «непредвиденной исключительной ситуацией». Путают или смешивают эти понятия, что не мудрено, ибо C++, C#, Java и др. идеологически используют единый подход для работы с ними: throw, try-catch-finally.
Какая же разница между этими понятиями? Простой пример. Допустим, вы хотите прочитать данные из файла. Эта операция может завершиться как успехом, так и может произойти ошибка: файл не существует, недостаточно прав и т.д. Все эти ситуации определены и описаны, они не должны приводить к падению и должны обрабатываться программой: выдать сообщение об отсутствии файла, запросить привилегии и т.д.
Возьмем теперь непредвиденную исключительную ситуацию: разыменование нулевого указателя. Никакой программист в здравом уме не будет делать это нарочно, это может произойти только в результате непредвиденной ошибки в коде. Предусмотреть подобные ошибки нельзя и они могут произойти в любом месте. Что же с ними делать? После такой ошибки практически невозможно восстановить нормальное поведение программы и единственное что можно сделать, это упасть как можно скорее и не усугублять дело, предварительно записав/отправив отладочную информацию (стектрейс, дамп и т.п.) и, по возможности, приведя данные к консистентному виду и освободив ресурсы. И да, я понимаю что в случае с веб-сервером, например, программа не должна падать, но смысл похожий — надо немедленно завершить обработку запроса и вернуть Internal Server Error.
Вернемся к Go. Он идеологически разделяет подход к работе с этими сущностями. Для предвиденных ошибок — это возвращаемое значение, это такой же результат работы процедуры и вы должны его обрабатывать (вы же не игнорируете результат, который возвращает процедура поиска первого вхождения подстроки в строке, даже если это "-1"?). Для непредвиденных исключительных ситуаций — это defer, panic и recover.
Go бескомпромисен, он побуждает, или даже, вынуждает программиста использовать те практики, которые считаются хорошими в мире Go. Если у вас вызывает дискомфорт такое отношение или если для вас хорошие практики отличны от того, что принято в Go, то конечно же у вас будет негативное впечатление от этого ЯП. Но это вопрос вкусов и к техническим деталям не относится. Да, Go местами сильно отличается от мейнстримовых ООП ЯП. Но разве не разумно иметь разный подход к работе с разнородными сущностями и не смешивать их?
> Возможно, некоторые не понимают разницу между «предвиденной ошибкой» и «непредвиденной исключительной ситуацией». Путают или смешивают эти понятия, что не мудрено, ибо C++, C#, Java и др. идеологически используют единый подход для работы с ними: throw, try-catch-finally.

Не понимают разницу, идеологически используют единый подход? В Java есть checked exceptions, unchecked exceptions и errors. И для каждого из этих типов есть свои подходы по применению и обработке (см. напрмер http://habrahabr.ru/post/183322/).

Да, в Java еще 20 лет назад осилили единый механизм обработки исключений. Но разве это плохо, для разных случаев и разных подходов использовать унифицированный механизм? Если бы кому-то было надо в Java, могли бы возвращать из методов Pair<Result, Exception>. Но вы вряд ли где-нибудь встретите такое, ведь это просто неудобно.
Да, в Java еще 20 лет назад осилили единый механизм обработки исключений.

Который не работает с лямбдами? Ведь если вы работали с Java'ой, вы знаете что checked exception's не дружат с лямбдами. Так же остается вопрос о передаче исключений между потоками (есть хорошая статья о FutureTask, но это не решение).
могли бы возвращать из методов Pair<Result, Exception>

В Java нет value типов, кроме примитивных(обещают в 10ой), а так же нет ADT(вообще не обещают), а значит возврат таких типов не несет особого смысла. Это кроме того, без поддержки замыканий, как части языка (что сделали сравнительно недавно) это все равно будет выглядеть криво.

Но вообще подобный подход использует RxJava со своим OnError.
> Который не работает с лямбдами? Ведь если вы работали с Java'ой, вы знаете что checked exception's не дружат с лямбдами.

Да, checked exceptions не дружат с лямбдами. Но разве это значит, что весь механизм обработки исключений не работает с лямбдами?

> Так же остается вопрос о передаче исключений между потоками (есть хорошая статья о FutureTask, но это не решение).

Что за статья и почему не решение? Напрямую с потоками в джаве мало кто работает. В современных фреймворках типа Akka, проблем с обработкой исключений нет.

> В Java нет value типов, кроме примитивных(обещают в 10ой), а так же нет ADT(вообще не обещают), а значит возврат таких типов не несет особого смысла.

Т.е. если что-то из перечисленного появится, пара результат-исключение станет более предпочтительна, чем try-catch?
Да, checked exceptions не дружат с лямбдами. Но разве это значит, что весь механизм обработки исключений не работает с лямбдами?

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

Что за статья и почему не решение? Напрямую с потоками в джаве мало кто работает. В современных фреймворках типа Akka, проблем с обработкой исключений нет.

Лично я использую RxJava — там механизм проброса исключений почти не используются. На андройде тоже приходится работать напрямую с потоками.

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

Станет рационален тип EIther например. И ошибку обработки файла, лично я, смогу обработать тут же. Но на вкус и цвет.
> Это значит, что механизм разделения на два вида исключений (обрабатываемые и необрабатываемые) провалился.

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

P.S. Если серьезно, то я не считаю, что идея с checked exceptions прямо полностью провалилась. В некоторых случаях (тот же I/O) польза от них была.
Идея с checked exceptions влияет на архитектуру принимающего кода. Она требует изменение всей иерархии вызовов, если изменилось возможные типы исключений. С современными IDE это не проблема, но порождает интересные ситуации в системе контроля версий. Да и вообще, вроде боролись с нагрузкой на программиста по прокидыванию ошибки, и к ней же и пришли.

Автор считает, что должно быть разделение на ошибки и исключения. В чем я его поддерживаю. Насколько удачно это реализовано в Go — это простор для личных размышлений. Мне симпатизирует подход Rust например в этом плане, но без макроса try! это бы выглядело ужасно.
> Автор считает, что должно быть разделение на ошибки и исключения. В чем я его поддерживаю.

Два сценария:
1. файл в папке не найден — это исключение, обрабатывается ифом
2. элемент в массиве не найден — это ошибка, panic, обрабатывается в recover.

Все логично, какие могут быть вопросы…
Что-то я не улавливаю по какому принципу вы разделяете эти два вида *исключительных ситуаций* и главное — зачем :-) Только не надо рассказывать сказки про «в одном случае можно продолжить работу, а в другом — конец света».
Так это вопрос к авторам языка. Я же привел классические примеры checked и unchecked exceptions из джава и то, как они выглядят в го. Похоже ребята уверенно наступают на те же грабли, что и джава 20 лет назад. Но там хотя бы был единый механизм обработки, а тут два разных.
Да, сори, это не вам ответ :-)
Неужели? Почему же тогда в комментарии, с которого началась эта дискуссия автор радостно объясняет нам важность такого разделения, да еще как будто в го первыми до него додумались.

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

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

Вот на этом месте остановимся по подробней. Где она успела провалиться? В C++ — based языках? Поправьте меня, если это не так.
Итак, давайте по полочкам.
Я писал уже и повторю еще раз, что С++, C# и Java (и не только — JavaScript, например) используют единую модель при работе с ошибками и исключениями: единый механизм для работы с ними — throw, try-catch-finally; общая иерархия классов ошибок/исключений (C#, Java); даже если вы захотите работать с ошибками как с возвращаемыми значениями, стандартная библиотека (и не только) будет упорно вставлять палки в колеса, ибо так не принято в экосистеме. Да, Java разделяет эти понятия через checked и unchecked exceptions, об этом написано чуть ли не в каждом учебнике, но использует опять же единые механизмы, да и реализация тоже не безгрешна: Mikanor привел уже с пяток проблем вызванных ей. И это данность.
Резюмируем. В C++ и C# вообще не разделяются эти понятия, в Java же хоть и разделяются, но используются единые механизмы для работы с ними, к тому же местами подводит реализация. Так от чего в Java провалилась идея разделения предвиденных ошибок и непредвиденных исключений? Может из-за реализации? Тогда к Go это не имеет ни какого отношения, там другая реализация, там совершенно различаются механизмы (это что касается вопроса «Зачем же это затаскивают в го?»).
И да, если абстрактный программист в вакууме утверждает, что «это плохой дизайн», «слишком много кода (проверок)», «мне нравится работать с ошибками вот так-то» — это вкусовщина, на вкус и цвет все фломастеры разные. Давайте оперировать фактами: «эта реализация приводит к таким-то и таким-то проблемам вот в таких случаях» или «это не работает вот в таких-то и таких-то случаях».
> Так от чего в Java провалилась идея разделения предвиденных ошибок и непредвиденных исключений? Может из-за реализации?

Отчасти — из-за реализации. Отчасти — потому что сама идея разделения исключений не проходит бритву Оккама. Checked exceptions — эта новая сущность, но никаокй пользы от ее введения нет. Это доказано последующими языками с таким же механизмом обработки (try-catch), но без checked exceptions. Оказалось, без них даже лучше.

> Тогда к Go это не имеет ни какого отношения, там другая реализация

А настолько ли другая? Что меня больше всего раздражало в checked exceptions:

1. компилятор обязательно заставляет их обрабатывать. Именно из этого и растут ноги того самого страшного try с пустым catch, которым теперь любители го попрекают джавистов. Боюсь, у вас скоро будет то же самое.

2. checked exceptions уродуют сигнатуры методов, препятствуют полиморфизму. Очень похоже, что в го то же самое, но не уверен.
Вернемся к Go. Он идеологически разделяет подход к работе с этими сущностями. Для предвиденных ошибок — это возвращаемое значение, это такой же результат работы процедуры и вы должны его обрабатывать (вы же не игнорируете результат, который возвращает процедура поиска первого вхождения подстроки в строке, даже если это "-1"?). Для непредвиденных исключительных ситуаций — это defer, panic и recover.

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

А defer, внезапно, это эмуляция finally/деструкторов С++ на стеке. Ни больше, ни меньше.

C++, C#, Java и др. идеологически используют единый подход для работы с ними

Нет, не используют. Вы, кажется, ни на одном не писали, раз так считаете. Есть исключения для исключительных ситуаций, которые не позволяют дальше продолжить нормально работу. Есть возвращающие значения функции как раз для подхода «ошибка это значение». Никто не путает и не мешает их, все на своих местах.
А defer, внезапно, это эмуляция finally/деструкторов С++ на стеке. Ни больше, ни меньше.

Не совсем — defer скорее аналог BOOST_SCOPE_EXIT — который тривиально реализуется в С++11/14, плюс время жизни в C++ ограниченно текущей областью видимости { и }, тогда как вызов функции в defer, в Go осуществляется только в конце самой функции, вне зависимости от того где был вызван defer внутри ее, что порождает забавные баги у новичков внутри циклов, например.
Почитайте про парсеры. И это таки видится более частным применением, нежели по прямому значению — завалить программу.

Благодарю за дельное замечание. Да, механизмы panic/recover используются в стандартной библиотеке для «передачи управления наверх» в сложных алгоритмах, все как вы написали (например, encoding/json/decode.go:199). Но это инкапсулировано в библиотеке и ошибка декодирования будет возвращена вам библиотечной функцией в качестве значения, а при непредвиденной исключительной ситуации произойдет panic (encoding/json/decode.go:139). Если есть механизм, отлично подходящий для решения какой-то проблемы, почему бы им не пользоваться? Но только это не должно влиять на клиентский код и интерфейс-то все равно должен соответствовать общепринятым в экосистеме рекомендациям.
Наверное, когда дело доходит до panic/recover, то ключевое слово здесь «наверх» (передача управления наверх). Не в вызывающую процедуру, а на верхний уровень библиотеки/обработчика запроса/программы.
может произойти ошибка: файл не существует, недостаточно прав и т.д.
Возьмем теперь непредвиденную исключительную ситуацию: разыменование нулевого указателя.
Всё таки не совсем понятно, как вы провели границу между «предвиденной ошибкой» и «непредвиденной исключительной ситуацией». Ведь всегда можно сделать
if(ptr==nullptr) {
    printf("error X");
}
Почему же нельзя предусмотреть подобную ошибку?
После такой ошибки практически невозможно восстановить нормальное поведение программы
Как и после любой ошибки, если она не была обработана, например почти нельзя восстановить нормальное поведение после попытки записи в несуществующий файл.
Мне кажется, корректней сравнивать не открытие файла с разыменованием нулевого указателя, а возвращение нулевого указателя (ошибка) и его разыменование (исключение). Тогда аналогично есть ошибка инициализации ресурса (идентификатора файла, например) и исключение при попытки обращения к такому идентификатору.
Благодарю, великолепный вопрос.
Когда вы вызываете процедуру, всегда может произойти ошибочная ситуация, и вызов процедуры не приведет к желаемому результату. Теперь подумаем, по какой причине это может произойти?
Например отсутствует файл, неверный формат входных данных, нарушен контракт вызываемым кодом (передан нулевой указатель) — все эти ситуации описаны в документации и должны быть корректно обработаны программистом, они просто являются результатом работы процедуры (предвиденные ошибочные ситуации): когда вы парсите текст — знаете что его формат может не соответствовать ожидаемому, когда открываете файл — знаете что он может отсутствовать и т.д. Более того, вы всегда знаете в чем причина такого поведения, как обработать такие ситуации и где они возникают — выдать сообщение что файл не найден (или создать его), что данные не соответствуют требуемому формату и т.д.
Если же мы возьмем ситуацию к которой приводит логическая ошибка в программе: опечатались, не закодировали возможную ветвь обработки, ошибка в расчетах и т.д., то это непредвиденное исключение, ненормальное поведение программы. Вы никогда не знаете когда и где оно возникнет. Вставка проверок тут не поможет — вы банально можете ошибиться при кодировании самой проверки, да и компилятор сам вставляет подобные проверки для отлова исключительных ситуаций. Если возникает такая ситуация, то лучшее что вы можете сделать (в общем случае) — это корректно завершиться (вы же не знаете где и что произошло и к чему это приведет, если продолжить выполнение, не можете предусмотреть все ситуации, ибо потенциально их очень много). Бороться с подобными ситуациями можно только одним способом — патчем, и только в таком случае нужен стектрейс чтобы отладить программу.
Надеюсь я ответил на ваш вопрос.
Какие ошибки «предвидеть», а какие — нет, зависит исключительно от задачи. Если я читаю JSON конфиг, то получение вместо него XML — точно такая же непредвиденная ситуация как и обращение за пределы массива. Если я исполняю пользовательские формулы, то ошибка в них — такая же предвиденная ситуация, как и отсутствие опционального файла. Не низкому уровню приложения решать какие ситуации являются предвиденными, а какие — нет.
Если я читаю JSON конфиг, то получение вместо него XML — точно такая же непредвиденная ситуация
Для вас будет неожиданностью, что формат конфига может не соответствовать заявленному? Да одна запятая, и все — парсер не поймет ваш конфиг. И XML тут вообще притянут за уши, для парсера JSON без разницы что конфиг — это валидный XML, главное что это не валидный JSON. Потом рассказывайте девопсу, что для вас стало неожиданностью когда он по ошибке подсунул программе чужой xml конфиг, вместо родного JSON, и программа вывалила стектрейс вместо вменяемого сообщения о некорректном месте в конфиге.

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

Не низкому уровню приложения решать какие ситуации являются предвиденными, а какие — нет.
Не припомню чтобы я писал о чем-то подобном. Не могли бы вы более развернуто описать это свое утверждение?
Не могли бы вы сбавить градус агрессии? Вменяемый парсер кинет исключение со вменяемым сообщением об ошибке и координатами некорректного места в файле. А у вас низкий уровень решает предвиденная ситуация (возврат ошибки) или нет (паника).
Вставка проверок тут не поможет — вы банально можете ошибиться при кодировании самой проверки
Выброс исключения тоже тут не поможет т.к. вы можете ошибиться при его выброса. Если вы можете выбросить исключение, значит вы можете и сразу обработать ошибку и вернуть невалидный результат. В таком случае процедуру нужно превратить в функцию, которая возвращает bool. Если же программист не предусмотрел какую-то ошибку и не обработал её — программа и так вывалится, компилятор об этом позаботится. Я не вижу, в каких случаях стоит ловить какие-то исключения. Если это segmentation fault — не за чем его ловить, просто падаем, если это пользовательское исключение — можно его заменить на возвращение invalid value.
Выброс исключения тоже тут не поможет т.к. вы можете ошибиться при его выброса. Если вы можете выбросить исключение, значит вы можете и сразу обработать ошибку и вернуть невалидный результат.
А я и не говорил что это делает программист в коде явно. В 99% случаев непредвиденные исключения выбрасывает рантайм: выход за границы массива, разыменование нулевого указателя и т.п.
Я не вижу, в каких случаях стоит ловить какие-то исключения. Если это segmentation fault — не за чем его ловить, просто падаем.
Расскажите это тем, кто эксплуатирует высоко нагруженные web-сервера, что если вы ошиблись при кодировании и какой-то запрос к серверу приводит в каком-то месте к выходу за границы массива, то в таком случае вы предпочитаете молча падать вместе с парой сотней одновременно обрабатывающихся запросов, не успев даже вернуть 500 — Internal Server Error в ответ на проблемный запрос.
… если это пользовательское исключение — можно его заменить на возвращение invalid value.
А описание ошибки не нужно?
В 99% случаев непредвиденные исключения выбрасывает рантайм: выход за границы массива, разыменование нулевого указателя и т.п.
Я об этом и спрашивал, в каких случаях есть смысл работать (выбрасывать/ловить) с исключениями для программиста? Следуя Вашей аргументации из предыдущего поста, программист не должен пользоваться исключениями — только компилятор/интерпретатор.
Расскажите это тем, кто эксплуатирует высоко нагруженные web-сервера, что если вы ошиблись при кодировании и какой-то запрос к серверу приводит в каком-то месте к выходу за границы массива, то в таком случае вы предпочитаете молча падать вместе с парой сотней одновременно обрабатывающихся запросов, не успев даже вернуть 500 — Internal Server Error в ответ на проблемный запрос.
Некорректное сравнение. Среда (Интерпретатор) скрипта как раз таки должна упасть, а веб сервер должен обработать падение среды, так и происходит. Тут не нужны исключения, веб сервер просто проверяет состояние скрипта (среды) и реагирует. В данном случае веб-сервер — это, в идеале, абстракция вроде виртуальной машины, исключения он не ловит.
А описание ошибки не нужно?
Если нужно — значит invalid value должно также поддерживать установку описания ошибки. Если следовать Вашей же логике, как я понял.
Если же в примере с web-сервером речь идёт о функциях уровня самого веб-сервера, то запрос должен проверяться на корректность, что и происходит, иначе как выбрасывалось бы исключение? А если запрос всё равно проверяется, почему не вернуть ошибку?
Код обработки ошибки — это не особенная конструкция, это полноценная часть вашего кода, как и все остальные… Ещё раз — ошибки это обычные значения, и обработка ошибок — это такое же обычное программирование.

Так, да не совсем так. На каждом уровне стека вызова обработка одних ошибок — полноценная часть кода, непосредственно к этому уровню относящаяся, а других — лишь сервисная, предназначенная для передачи ошибки на уровень выше, как правило мешающая пониманию полноценной части кода, захламляющая его.
Вот тоже хотел написать, прелесть исключений в том, что они ловятся на том уровне, который их реально может обработать, не захламляя все нижележащие. И это, пожалуй, их главное преимущество, о котором почему-то умачивается. Те, кто писал более-менее сложный код на С (в ядре ОС, к примеру), обычно очень хорошо понимают весь ужас C-style error handling.
Те, кто писал более-менее сложный код на С (в ядре ОС, к примеру), обычно очень хорошо понимают весь ужас C-style error handling.

Некорректно сравнивать C-style и Go-style error handling.
Понятно, что первый поверхностный взгляд увидит некоторую схожесть (вовращаемые значения), но в C ошибки — это не значения, это коды ошибок, и это да, ад. В Go, помимо наличия множественных возвратов, ошибки — это полноценные самодостаточные объекты, и это кардинальная разница.
Разница лишь в том, что в одном код ошибки, а в другом составной объект. Это все тот же механизм ошибка=значение. Ну и никто не мешает возвращать структуры в С в качестве ошибок и получить вообще копию Go подхода. Но так не принято, не более того. Поэтому сравнивать корректно, потому что механизмы за ними стоят идентичные. Можно себя долго обманывать и искать в Go новизну, но как уже множество раз говорилось авторами языка — в нем нет ничего нового.
Это все тот же механизм ошибка=значение.

Тот же, да не тот же. Повторюсь, сказать «А похож на Б, следовательно имеет те же проблемы» — грубая логическая ошибка.

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

Я так понимаю, из моей фразы в статье «подход Go тут является и зрелым и свежим одновременно» вы выбрали слово «свежим», проигнорировали слово «зрелым» и пытаетесь обвинить меня в предвзятости. Непонятно зачем только вы так тратите своё время.
Может расскажете, наконец, чем он не похож? Не томите нас :-)
Как же утомило это паясничание.

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

Как человек, который долго писал на С, и достаточно долго пишет на Go, вижу, что это big deal, и эти различия кардинально меняют комфорт и ход работы. А вы на чем пишете, что не можете/не хотите увидеть различий и их последствий на практике?
В си тоже можно запаковать несколько значений в одну структуру, но так делать не общепринято, да. Тем не менее, возвращение структуры ErrorInfo — не выглядит каким-то кардинальным преимуществом перед возвращением примитива ErrorCode.

Я много на чём пишу, но подход Лиспа мне видится наиболее толковым. Там решение о раскрутке стека или игнорировании принимается высокоуровневым кодом, но в момент возникновения условия на низком уровне. Получаем и большую гибкость, и скорость.
Во-первых, нет принципиальной разницы между возвращением int или struct в качестве индикатора ошибки. Во-вторых, вы это можете делать в обоих языках, так что C-style и Go-style — это, по сути, одно и то же: в обоих случаях вы либо должны обработать ошибку по месту вызова функции, либо проигнорировать её, либо вручную передать её выше.
Во-первых, есть принципиальная разница — в Go возвращается не struct, а интерфейс — этого вы в C не можете сделать по определению. Да, в некоторых С-библиотеках используются error-типы, но сценарий их применения достаточно ограничен.
Во-вторых, детали имеют значение — там где в С было просто забить на код возврата, в Go, чаще всего, нужно сознательно забить, еще и написать _ и проигнорировать ругательства линтера.
В том примере, который вы привели в статье, забить на проверку ошибки как раз ну очень просто.
Ясно.
Хоть лбом об стенку бейся ))
Ясно.
Очень приятно, что вы наконец осознали, что привели пример, в котором проигнорировать ошибку просто.
Хоть лбом об стенку бейся ))
Пожалуйста, не бейтесь головой об стену, это не принесёт пользы никому. Лучше напишите статью, которая продемонтстрирует каким образом в Go можно сделать игнорирование проверки ошибки сложной задачей. Также пожалуйста раскройте подробнее в каких случаях применяется подход подобный приведённому в вашей текущей статье, а в каких случаях тот, который усложняет игнорирование ошибок.
Смотрите, статья о том, что в Go вы не относитесь к ошибкам, как к чему-то лишнему и код обработки ошибок не воспринимаете, как что-то, что «захламляет остальной код». И этот ведёт к тому, что у вас не стоит вопрос «как не игнорировать ошибки».
Поэтому ваши комментарии в стиле «в примере на counter просто забить» — это именно то, что говорит, что вы не поняли посыл статьи, и продолжаете мысли своим «докажите, что Go мне не даст забыть про ошибку». Что, впрочем, лучше, чем «не хочу захламлять код проверками ошибки» :)
Поэтому неудивительно, что там так прижился goto. Без него теже ресурсы освобождать в каждом if error != 0 это страх и ужас, который обязательно закончится утечкой ресурсов, которые забыл освободить в очередном обработчике ошибок.
Если Вы про С, то ничего там не прижилось. Стиль зависит от конкретного разработчика, как и в других языках. То, о чём Вы говорите, можно делать и так:
do {
    ....
    if (error) {
        break;
    }
    ....
    if (error) {
        break;
    }
    ....
} while(false);
/* Release resources here. */
....
И получился более страшный вариант кода с goto на метку cleanup. Так или иначе нужно делать то, что делают деструкторы в С++ и finally, если хочется получить что-то читабельное.
Кажется, не только более страшный, но и более хрупкий. Полгода спустя один из этих if окажется во вложенном (настоящем) цикле.
Есть альтернативный способ обработки ошибок на нужном уровне — монадический, как в функциональных языках. Не знаю, правда, насколько Go это одобряет.
Настолько же, насколько любой другой язык без поддержки алгебраических типов данных.
Вроде бы достаточно обобщённых структур и лямбда-выражений.
Generics в Go нет, это одна из самых горячих тем около Go.
Ну тогда да, печаль
Спасибо за статью))) Добавил в избранное.
Я и на С++ так делаю. И в общем программы построены так, что для любого типа есть какое-то недействительное значение, которое обрабатывается наравне с действительными в общей логике. Исключения в основном только там, где того требует внешнее API.
Когда понадобилось написать одну программку на C#, категорически не понравилось то, что функция преобразования строки в число выбрасывает исключение, если строка — не число. Ну да, в принципе логика понятна — не число же, но что мешало сделать хотя-бы две версии функции — со значением по умолчанию и с исключением. На практике обе эти ситуации необходимы одинаково часто.
Помимо 'int int.Parse(string)' есть ещё версия 'bool int.TryParse(string, out int)'
Как раз для вашего случая.
Ну да, я на тот момент не знал об этом:)
Но там еще такие штучки были. Не помню точно, вроде точка или запятая как разделитель при вводе числа в ячейку таблицы, причем это зависело от текущей локали, при неверном вводе тоже что-то вываливалось.
Нет, TryParse в этом случае тоже не вывалится. Но вернёт false, так что придётся делать два вызова с разными локалями
divan0 > Подход Go тут является и зрелым и свежим одновременно, он чрезвычайно прост и при этом непривычен для понимания, и требует определенных усилий, чтобы прочувствовать всю мощь. Но, что самое важное — он отлично работает на практике.

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

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

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

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

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

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

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

Исключения позволяют отделить обработку того, что к логике мало относится — оно отделяет обработку исключительной ситуации

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

Я не вижу ее и в Go и ваши статьи каждый раз это только подтверждают. Так что паритет.

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

Вправду хотите знать? Я не вижу смысла общаться с человеком, который так возносит себя над всеми остальными, включая пионеров computer science, унижает их и новые для себя технологии, лишь из-за того, что привык работать с другими технологиями. Никогда не понимал таких людей, и таких яростных я ещё встречал. Удачи в борьбе с Го и несогласными с Вашим Единственным Истинным Мнением.
Я не вижу смысла общаться с человеком

Так вы может просто уже прекратите это делать? Раз сказали — ладно, сорвались, бывает. Второй раз сказали — ну блин, надо что ли яички уже отрастить, либо просто не бросаться громкими словами.

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

Врать не хорошо. Обижаться можно и молча.
ИМХО:
Низкий уровень визжит от радости системой ошибок в Go. Очень удобно.
Прикладной уровень, где каждая ошибка это просто падение всего приложение\его части — люто его ненавидит.

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

// До
func SendMessage(db *gorm.DB, user S.User, rid uint, text string) (*S.Message, error) {
	if !user.registered {
		return nil, fmt.Errorf("User is not registered")
	}
	if !user.canSendMessages {
		return nil, fmt.Errorf("User can't send messages")
	}

	message := S.Message{}
	db.Create(S.Message{ ... })
	if db.Error() != nil {
		....
	}
	return &message
}

// После
func SendMessage(db *gorm.DB, user S.User, rid uint, text string) S.Message {
	E.BS(!user.registered, "User is not registered")
	E.BS(!user.canSendMessages, "User can't send messages")
	
	message := S.Message{}
	E.DS(db.Create(S.Message{ ... }), "Couldn't send message")
	return message
}


Внутри E.BS и E.DS происходит проверка на буль\ошибку в db, кидание паники вместе с именем файла, номером строки и переданной строчкой-описанием.

Выше это ловит обработчик транзакций и откатывает всю транзакцию к бд, а потом выше ловит роутер и пишет 500 в ответ на запрос. Соответственно есть и E.Throw, E.Catch и E.Rethrow. Работают паники в Go достаточно медленно, чтобы это можно было заметить, но я готов пожертвовать этим временем ради того, чтобы не писать такие нечитабельные кусты из условных переходов и return nil, err (а если первое возвращаемое значение — не указатель? мммм)
довольно интересный подход

можно немного больше подробностей?
или может есть возможность глянуть исходники на гитхабе?
Где-то через месяц я напишу несколько статей на тему того, к каким здравым и не очень решениям привела меня моя лень в сфере Go и Obj-C. Там же и все опишу. Исходников на гитхабе пока нет.
fmt.Errorf(«User is not registered»)

Никогда, ради всех ваших потенциальных и текущих коллег, так не делайте. Это делает невозможной интроспекцию ошибок сторонним разработчиком. В вашем случае, поскольку ошибка не несет метаинформации — errors.New и global var позволит тем, кто работает с вашим кодом легко узнать, что именно за ошибка произошла у вас внутри — и корректно ее обработать. Если же вы планируйте добавить дополнительную динамическую информацию, заведите новый тип удовлетворяющий интерфейсу error (всего один метод). Вам потом скажут спасибо.

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

Я завожу новый тип, удовлетворяющий интерфейсу, обычно. Код выше — пример =).

E.BS(
E.DS(

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

fmt.Errorf(«User can't send messages»)

Про Errorf уже выше написали, что да, подобные «строковые» ошибки лучше либо выделять в отдельные переменные, либо создавать свой тип. Вообще, мне понравилось предложение из issue по поводу интроспекции ошибок в stdlib:
Если бы я был королем, я бы заставил всех авторов библиотек следовать двум правилам:
— errors.New() использовать только для объявления новых публичных переменных (типа var ErrNoRows ...)
— если вам таки нужно использовать fmt.Errorf() — не используйте, а сделайте кастомный тип для этой ошибки

Очень здравый взгляд, как по мне. Во втором случае имеется ввиду, что Errorf() выполняет некоторое форматирование, значит есть какие-то виды/стейты/данные в этой ошибке -> есть смысл создать типа для этого вида ошибки.
Я согласен с тем, что функции E.BS и E.DS совершенно не читабельны и не ясны.

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

Повторюсь, дело в уровне.
Если бы эта часть проекта была написана на языке, поддерживающем исключения, то на месте E.DS и ко не было ничего, а на месте E.BS какой-нибудь Assert\Throw. И никаких try catch тоже не было здесь. Тут мы просто падаем вверх по колстэку после любого малейшего чиха, и покуда этот чих не будет распечатан в лог, нас совершенно не волнует, что это вообще такое было. Т.е ни одна из брошенных таким образом ошибок никогда не будет обработана каким-либо образом, отличным от «распечатать лог, сбросить транзакцию, кинуть 500».

Теперь представьте код обработчиков запросов серверного приложения. 40 возможных запросов API, 40 обработчиков, в каждом примерно 15 полезных (не обработка исключительных ситуаций) строчек кода, половина из которых может «ошибиться». Т.е каждой нужно воткнуть if err, каждой функции два возвращаемых значения, еще думать про различные типы ошибок и их как-то обрабатывать и бороться с синтаксисом языка, не подразумевающим возможности просто вернуть ошибку (второе возвращаемое значение) без указания первого, кроме как сделать его указателем (return nil,… ) или использовать именнованные возвращаемые значения.

Это — ад, товарищи. Error — инструмент, предназначенный совершенно не для этой работы.

Я хочу видеть в этом конкретном случае вот такой код
func registerNewUserHandler(nickname string) User {
	checkAvailability(nickname)
	user := addNewUserToDB(nickname)

	addLuckyBonusPoints(user)
	notifyEverybodyAboutNewcomer(user)

	scratchThePonies()
	return user
} 


и просто помнить, что любая строчка может вывалиться, а не вот такой:

func registerNewUser(nickname string) User, error {
	if !isAvailable(nickname) {
		return User{}, /* Whatever error style you guys prefer */
	}
	user, err := addNewUserToDB(nickname)
	if err != nil {
		return User{}, err
	}
	err = addLuckyBonusPoints(user)
	if err != nil {
		return User{}, err
	}
	err = notifyEverybodyAboutNewcomer(user)
	if err != nil {
		return User{}, err
	}

	err = scratchThePonies()
	if err != nil {
		return User{}, err
	}
	return user, nil
} 


Просто потому, что обработка ошибок тут — шум и не несет никакой полезной нагрузки. А потом оно еще уходит вверх и так же плодит в коде, вызывающем эту функцию, условные переходы. И зачем?
Ну, мы тут уходим в дискуссию «исключения vs возвращаемые значения», а она на много страниц будет :)
Но первый код экономит строчки, но прячет полностью логику того, где и что происходит (и происходит ли) во время ошибки в любой из тех функций. Это здорово поначалу, но по мере того, как над кодом работают несколько поколений программистов, и код рефакторится несколько раз — обработка ошибок оказывается слабым местом. Второй код более многословный, но дает вам(и другим программистам, которые придут после вас) сразу всю картинку целиком, дает гибкость в реализации различного поведения в каждом случае, и, что самое главное, делает ошибку — таким же полноправным членом функции, о котором нужно заботится и понимать, зачем она тут и что с ней будет.
Был бы какой-нибудь оператор returniferrnil User{}, err — претензий было бы заметно меньше.
В дискуссию «исключения vs возвращаемые значения» вы ушли как только написали статью. Или даже раньше, когда перевели статью Пайка.
Может быть, тогда не стоило называть это ошибками? А назвать более понятно — возвращаемый результат. И тогда не нужны такие вот прекрасные статьи о том, что ошибки — это не совсем ошибки, не будет необходимости разжёвывать тормозной публике свежие и зрелые идеи, конфликтующие со здравым смыслом и т.п. Уж лучше смешивать ожидаемые «ошибки» с неожиданными, чем эти «ошибки» с другими значениями.
Так это и есть возвращаемый результат. Но его суть — информация об ошибке.
В принципе, из названий — только интерфейсный тип error, которому может удовлетворять любой тип, имеющий метод Error() string.
Т.е. ошибок как таковых нет, есть только возвращаемые значения, некоторые из которых условно называются ошибками. Я правильно понял концепцию? Изучая Go, я каждый раз испытываю огромное внутреннее сопротивление. Да, я в курсе как оно объясняется представителями элиты, но от этого не легче. Причем, что интересно, когда я пытаюсь писать код «просто так», изучая те или иные аспекты, всё гладко и даже приятно. Но как только пытаюсь решать боевую задачу, начинается ломка мозга и возникает ощущение что я делаю что-то противоестественное.
Именно. Ошибка — это такой же полноценный член семьи, как и любой другой. Это значения, в которых вы вольны хранить/передавать делать все что угодно. Насчёт ломки — могу только пожелать быстрее через неё пройти. Сам писал раньше с исключениями, но мне никогда такой подход не был по душе, поэтому Go я быстро принял.
Тогда для чего это называется «ошибкой»? Я вернулся к исключениям как к более понятному и естественному для меня способу обработки ситуаций, которые не ведут к желаемой цели.
Тогда для чего это называется «ошибкой»?

Потому значение всё же представляет ошибку. Вы же не спрашиваете «почему тип User называется юзером?».
Вопрос не к этому. Почему нет отдельных статей, посвящённых тому, что User — это просто значение, что Product — это значения, что abc — это тоже значения? Чем замечательна эта переменная, которую назвали err, что из-за неё пишут статьи и строчат комментарии?
Хороший поинт. Кратко — потому что есть иные взгляды на обработку ошибок в других языках, которые, по мнению авторов Go (и не только их) принесли лишь дополнительную сложность и путаницу, и именно от этих взглядов так сложно избавится.

Как я понимаю, концепция «ошибки как значения» — естественная, её не нужно изобретать, это то, как мозг интерпретирует задачу. Но тут огромную роль играют конкретные аспекты конкретных языков. В C, например, возвращали код ошибки и дальше работали с ним — это было неудобно, многословно, это была лишняя сложность. Очевидно, что теория языков программирования не стояла на месте и пробовали другие подходы, где ошибки старались трактовать «особенно», придумывая для них какие-то новые сущности и инструменты. На практике у каждого подхода есть и плюсы и минусы, причем сильно завязанные на реализации в каждом конкретном языке.

В Go — в духе стремления к простоте, проверенным практикам и дизайну, ориентированному на конкурентные(concurrent) программы — пришли к такому дизайну, в котором используется возврат значений, но лишенный тех минусов, которые были в других языках, и обогащенные особенностями Go. Это быстро, это гибко, это делает очень понятным и простым (пусть и чуть более многословным) код. Так авторы Go видели наилучший подход на тот момент, и многие его находят очень практичным и удобным.
Идея понятна. На мой взгляд, это глобальная проблема Go. Есть несколько уровней простоты. На одном из низких уровней Go действительно прост — мало сущностей, простые концепции и т. д. Но по мере увеличения системы возникает необходимость поддерживать эту простоту, а это уже сделать гораздо сложнее, чем если бы низкий уровень был хотя бы чуть более сложным. Такая вот загогулина.
Да, но согласитесь увеличение сложности, связности и глубины абстракций — это вопрос архитектуры и дизайна. На Go не пишут гигантские энтерпрайз-монолит системы, но пишут гигантские распределенные системы. Это не дань моде, это реальная потребность современных систем.

Возможно, Go далеко не лучший язык для написания раздутого гигантского монолита, да. Но он отличен для создания распределенных систем, наверняка использующих SOA дизайн, кодовые базы которых при этом остаются достаточно гибкими для роста, рефакторинга и работы больших групп программистов.
В моём случае Go-проект стал слишком быстро неуправляемым по сравнению с symfony-проектами гораздо больших масштабов. И не последнюю роль в этом сыграла обработка ошибок. Мучительно больно было тащить if err != nil из недр какого-нибудь высокоуровневого валидатора в веб-сервер. Видел различные решения-обёртки для отлова таких ошибок, но их идей я не смог понять. Это примерно то, о чём я написал в предыдущем комментарии: из-за чрезмерной простоты языка требуется создавать чрезмерно сложный дизайн приложений.

Да, я знаю что в вашей тусовке это классифицируется как РНР головного мозга, но так уж вышло. И, думаю, я не одинок.
Это то, о чём пишет Пайк в свой статье — обработка ошибок в Go это не «паттерн if err != nil».
окей, я действительно неправильно выразился, if err != nil это обработка не-ошибок. А я про обработку ошибок имел в виду. Т.е. когда err действительно не равен nil, и это надо вывести наружу
Вероятно, более уместно было бы тогда назвать статью (и всю концепцию) как-то вроде «В Go нет ошибок», ну и раскрыть ниже: в этом языке нет ошибок, и соответственно, нет инструментов их обработки. Если вы хотите по привычке «обработать ошибку», возьмите любую переменную, назовите её err и договоритесь, что если в ней что-то есть, то это будет «ошибкой».
Вот это хорошо звучит, хотя «В Go нет ошибок» совсем вызывающе звучит — как никак но интерфейс error — это часть языка. И возьмите не «любую переменную», а «нужную переменную, которая содержит всю необходимую вам информацию об ошибке в нужной вам форме». Но так да, суть верна.
Я хотел бы увидеть ещё одну статью, посвящённую обработке ошибок как значений, пробросу этих ошибок вверх и борьбе с неизбежно возмникающим при этом дублированием кода.
Сколько можно генерировать статьи на одну и ту же тему?
Философия не изменит факта — обработка ошибок в Go сделана примерно как в C, только слегка удобнее. И ей очень далеко до исключений в высокоуровневых языках.

Можно было бы применить монады, как cntWriter в примере, но вот для них нужно обобщенное программирование, которого в Go нет. А генерировать подобные структуры для каждого типа — невозможно. Кроме того без обобщенного программирования не получится нормально такие недо-монады композировать.

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

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

Можно было бы применить монады

В Go нет монад. В испанском языке нет кириллических букв. Don't fight the language.

Получился в итоге язык, который был бы очень востребован 25 лет назад.

SpaceX, Dropbox, Cloudflare и много других взрослых компаний с вами не согласны.
Нет, между Go и C огромные отличия, вы же видите только схожести и делаете выводы только из них.

Отличия в философии, но не в коде. В этом и проблема. Философия очень тяжело идет если кодом не подтверждается.

В Go нет монад.

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

>> Получился в итоге язык, который был бы очень востребован 25 лет назад.
SpaceX, Dropbox, Cloudflare и много других взрослых компаний с вами не согласны.

Не согласны в чем? Они считают что Go не был бы востребован 25 лет назад?

Все эти компании используют Go по прямому назначению — как замену С. Только 25 лет назад это было бы почти 100% рынка, а сейчас от силы 10% наберется. За 25 лет придумали много хороших языков и Go вряд ли сможет из потеснить.
Нет, между Go и C огромные отличия, вы же видите только схожести и делаете выводы только из них.

Пожалуйста, назовите их. В этот раз обойдусь даже без хероты и извращений.
Если бы у бабушки были… упс, кто-то уже сказал это до меня…
UFO just landed and posted this here
Для всех этих средств нужно обобщенное программирование. Которого в Го нет. И это не принципиальный момент — его просто не знают как сделать. Концептуальных реализации было несколько — начиная от связи с интерфейсами, заканчивая value T. Все имели свои недостатки, и не давали существенных преимуществ.

Под обобщенным программированием всегда идет очень большой комплект теории, в том числе и применимости к языку. Это не тот вопрос, который можно решить по щелчку пальцев. И оно нужно. Вопрос в том, как его внедрить, что-бы потом не плеваться, и не решать возникшие проблемы (которых уже есть), и не шарахаться от резко увеличившейся сложности. Нашедшему ответ поставят памятник.
UFO just landed and posted this here
Давайте будем честны. За 25 лет существования Haskell'я он так и остался достаточно экзотичным, хотя и приятным языком. Это не делает его хуже\лучше. Но факт в том, что подавляющему большинству программистов чистые функциональные языки даются по прежнему тяжело. Возможно причина в том, что мы сами привыкли в жизни раскладывать на последовательности действий. Возможно причина в том, что машина, с которой мы работаем, по умолчанию императивна, и этому обучают в ВУЗах любой страны. Возможно в чем то другом.

А реклама это не плохо — эти статьи выходили с большим перерывом между ними, поэтому не вызывали такого буйства эмоций. В конечном счете — любые теоретические обсуждения не имеют особого смысла без практического опыта внедрения и применения. Как наверху заметили — у Go он есть, даже с точки зрения компаний совсем не связанных с гуглом. И если он есть, и они продолжают его использовать — значит в большинстве своем они довольны результатом. А что еще нужно?
UFO just landed and posted this here
Не проецируйте мнение и манеры общения пары человек на все сообщество. Иначе грустная картина о любом языке выйдет. Во вторых вы спорите с элитизмом, но с позицией элитизма, что лично я нахожу забавным. С одной стороны у вас «старапы для кошечек» которые и раз и рождают тот самый хайлоад, инструменты работы с которым мы имеем. По сути youtube есть один большой кошатник — не делает его уроки полученные программистами менее полезными. С другой «великолепный опыт» и «великолепный Пайк» который вызывает у вас отторжение.

Касаемо рекламы — вас никто не застовляет читать статьи про Go, ведь правда? Исходя из вашей логики, время ведь можно потратить куда эффективнее.
UFO just landed and posted this here
Хабр это коллективный технический ресурс. Сюда пишут все — кому и что интересно. Если уж на, что и стоит пенять, так это на невозможность заблокировать определенные теги в ленте. Ну это уже к администрации сайта.

Проблема математического обоснования в том, что оно подходит для сферической системы где учтено все. Ну и в том, что его долго и сложно делать. А вопрос, что элитнее это вопрос личных вкусов и его можно продолжать до конца веков. Как и спор ошибки vs исключения который поднят здесь, хотя впервые поднят был еще лет 20-30 назад. На программирование много разных взглядов, и единого мнения даже по самым банальным вопросам у нас нет.
Надо сделать отступление и сказать, что у Пайка многие статьи специфические. Как и его позиция по некоторым вопросам. Это не делает эту позицию хуже или лучше, она просто является позицией одного автора(да, у Го три автора а не один). С которым не обязательно соглашаться, разрабатывая на Go.

Касаемо ошибок — Го продвигает простую идею. Если на ошибку можно адекватно среагировать, это нужно сделать как можно скорее. Для этого есть множественный возврат, интерфейсы, type assertions. Для ошибок такого рода не нужен обязательный stacktrace т.к. они являются обрабатываемой ситуацией. если же дело идет о фатальных и около-фатальных ошибках то есть механизм panic\recover которые автоматически будут проброшены по всему стеку. Кстати есть хорошая функция runtime.Goexit позволяющая документировано завершить работу любой горутины.

Касаемо исключений — в императивных языках есть сложности при передачи их между потоками. С учетом того, что Go изначально сделан с упором на быстрое создание легких сопрограмм — механизм исключений, как основа, вызвал бы определенные сложности при их обмене. error как значения легко передают по каналам. Механизм panic\recover по многим горутинам уже разнести проблема.
Навскидку — БД умерла. Скакануло напряжение. Это фатальная ошибка. Другой пример это файловая система — начали умирать HDD. Тоже фатально, тоже надо чинить руками. Третий это нарушение логики работы приложения по вине программиста — и такое бывает, программисты не боги — абсолютно все случаи учесть невозможно. К rest это относится меньше конечно.

А если из практики, то это ошибки других сервисов, во время транзакции при оплате товара, как выше сказали. Гораздо лучше откатить покупку, чем разбираться потом с разъяренным клиентом.
почему нельзя отреагировать на смерть БД или ошибку другого сервиса?
if (db.ok()==false) {
  printf("db is dead");
  return false;
}
if (remote.fail()) {
  rollback();
}
«Скакануло напряжение» — это не ошибка. Программа не может знать о «скачке напряжения», только о статусе датчиков, ParseError или «Segmentation fault». На статус датчиков и ParseError можно отреагировать. А на «Segmentation fault» действительно лучше падать, но тут и делать ничего не надо. Я веду к тому, что нет таких случаев, когда нельзя выброс исключения заменить на «адекватно среагировать».
Скакануло напряжение это объяснение смерти БД было. То, что вы привели это не обработка фатальной ситуации, более того такой код скорее всего породит ошибки на функциях внизу по стеку, т.к. те будут пытаться повторить операцию (bool == false штатная ситуация).
Такой подход не работает.

if (db.ok()) {
  db.read(...) 
}

Вот только межу проверкой и чтением база может упасть и функция read должна както сигнализировать вызывающему коду об этом. И мы возвращаемся к исходной проблеме — как написать код, который не игнорирует ошибку и не позволяет программе выдать некорректный результат.
Выброс исключений тут ситуацию не меняет. Потому что всегда можно заменить его на возвращение результата, который isInvalid().
И что потом делать с этим? Что ты можешь сделать если IsInvalid() == true? Пробросить наверх? Тогда с исключениями вообще ничего писать не надо.
Об это и речь — нет никакого разумного способа четко разделить, где «на ошибку можно адекватно среагировать», а где «дело идет о фатальных фатальных и около-фатальных ошибках».
В данном случае зависит от ситуации. На из isInvalid() можно отреагировать возвращением в предыдущее состояние, выводом в лог, повторной попыткой и т.д.
То есть обработка ошибки зависит от внешних факторов. Поэтому нужно бросать исключение, чтобы ошибку нельзя было проигнорировать. Иначе велика вероятность получить некорректную программу.
в каких случаях тогда не нужно бросать исключение? При поиске подстроки, нужно возвращать -1 или исключение? Но ведь обработка зависит от внешних факторов…
Интересный вопрос, в двух словах не ответишь.

Начнем с аксиомы — система обработки ошибок должна не позволять пользователю ошибки игнорировать по-умолчанию. Любые паттерны игнора ошибок должны быть однозначно отлавливаемыми статическим анализом. В этом плане исключения работают идеально.

Но у исключений два недостатка:
1) Исключения работают медленно
2) Синтаксис try\catch довольно громоздкий и вообще не является выражением

Из этого можно построить модель:
  1. По умолчанию любой API должен бросать исключение (panic)
  2. Не бросать в случаях когда «ошибка» присутствует в протоколе. То есть «ошибка» является частью ожидаемого поведения. Например в web клиенте не бросать исключение при кодах ответа != 200.
  3. Для тех случаев когда выполняется сразу несколько условий:
    • накладные расходы на бросание исключения велики по сравнению с телом метода
    • «ошибочное» значение появляется достаточно часто
    • «ошибка» не является внешней, то есть может быть формально исключена на стадии разработки
    Только в этих условиях можно возвращать значение вместо ошибки. Причем именно значение как результат функции, а не как дополнительное значение «ошибки».
    Сюда попадают всяческие indexOf, TryParse, TryLock и прочите Try или OrDrefault идиомы.


Есть и менее радикальный способ, в котором можно оставить возврат «ошибок» как значений. Но нужно тогда иметь pattern-matching как в ФЯ. И на любой incomplete\empty match ругаться. Но тогда нужны монады и некоторая do-нотация, чтобы не загромождать код.

Например так:
try x := write(...)

match write(...) 
_, err -> return err,
x, nil -> //...


чтобы оно автоматом переписывалось в
x,err:=write(...)
if(err != nil) { return err; }

y,err:=write(...)
if(err != nil) { return err; } else { ...}


Лучше так:
index := «xxx».indexOf( «porn», { absent: -1 } ) // возвращаем дефолтное значение
index := «xxx».indexOf( «porn», { absent: NoPornException } ) // кидаем высокоуровневое исключение
Я с Вами согласен, но это не имеет ничего общего с разделением ошибок на фатальные (исключения) и нефатальные (возвращаемые), как предлагал Mikanor. Вы предлагаете бросать исключения всегда, кроме тех случаем, когда ошибка — явная часть возвращаемого типа. Либо когда из-за проблем производительности приходится использовать возвращаемые значения вместо исключений, что является вынужденным хаком.
«ошибка» не является внешней, то есть может быть формально исключена на стадии разработки
Только в этих условиях можно возвращать значение вместо ошибки. Причем именно значение как результат функции, а не как дополнительное значение «ошибки».
Я не понял, что Вы хотели сказать. В случае indexOf, "-1" как раз является дополнительным значением ошибки таким же как nullptr. Если программист не вставит проверку на -1, можно получить UB.
«Фатальность» ошибки — вещь относительная. В одной программе отсутствие файла — фатальная ошибка, потому что программа не рассчитана на такие обстоятельства. А в другой файл просто создается.
Но создателю API нужно предусмотреть оба случая, поэтому в случае если функция не может сделать что должна — надо кидать исключение (panic).

Я не понял, что Вы хотели сказать. В случае indexOf, "-1" как раз является дополнительным значением ошибки таким же как nullptr.


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

Если программист не вставит проверку на -1, можно получить UB.
UB это термин из мира C++, там много дырок в спецификации. В большинстве языков отрицательные индексы массива явно запрещены, поэтому отсутствие проверки на -1 приведет к index out of range exception в следующей строке.

Кстати у значения -1 функции indexof есть еще один смысл. Когда программист пишет код получения подстроки начиная с некоторого символа (задача примитивного парсера), то получается так:
var index = str.indexOf(x);
var sub = str.substr(x+1, str.length-index-1);

Этот код прекрасно работает, даже когда x не найден в строке. Любое другое значение усложнило бы подобный код.
«Фатальность» ошибки — вещь относительная.
К тому же, согласно вашему предыдущему посту, фатальность не является критерием для выбора между ошибкой и исключением. Вы предлагаете использовать в качестве критерия свойства возвращаемого типа. Если тип явно предусматривает значение-ошибку возвращать ошибку, иначе — исключение.
Причем «ошибка» является одни из значений функции, а не дополнительным. Так сложнее игнорировать.
Почему сложнее? Наоборот даже. Что Вы имеете ввиду под «одним из значений функции, а не дополнительным»? Что возвращается тот же самый тип, что и обычно, но значение сигнализирует об ошибке (как nullptr)? Альтернатива — возвращать другой тип или пару?
B это термин из мира C++, там много дырок в спецификации. В большинстве языков отрицательные индексы массива явно запрещены, поэтому отсутствие проверки на -1 приведет к index out of range exception в следующей строке.
За это приходится платить производительностью. UB — это не дыры в спецификации, а сознательный выбор обусловленный тем, что для выброса исключения нужно проверять аргумент, то есть вставлять дополнительный if (index<0) {throw;}, что не всегда необходимо на самом деле т.к. во многих случаях эту проверку можно вынести на более высокий уровень, за пределы цикла, например. Для решения этой проблемы существует assert(), который проверяет индекс в debug версии, но не делает ничего в release. Имхо, это решение лучше, чем перманентный неотключаемый debug в тех языках, где выбрасывается исключение «index out of range».
Если тип явно предусматривает значение-ошибку возвращать ошибку, иначе — исключение.
Не знаю ни одного типа, который вообще выажается терминами «ошибка». Если делается новый тип, то любое его значение корректно.

Но типы это еще не все. Возьмем тот же indexOf, что должно вернуться в случае nilв качестве параметра? Код ошибки? Нет, нужно падать.

Поэтому в первую очередь важно поведение, а не типы значений.

Почему сложнее? Наоборот даже.

Ну вот представим функцию indexOf, которая возвращает null если подстрока не найдена, любое обращение к null вызовет NRE. А теперь та же функция, но возвращает ошибку в дополнение к значению (как чаще всего встречается в Go), тогда каждый второй напишет
res, _ := indexOf(...)

И получит некорректное поведение.

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

За это приходится платить производительностью.
Мы сейчас о корректности, какой смысл иметь быструю программу, которая работает некорректно? Тогда проще на ассемблере или голом C писать, точно ни одной лишней операции не будет.

Что касается производительности, то с range-based циклами можно проверку границ выбрасывать, она гарантированно не выполнится, а для простых обращений — оставлять. Потри быстродействия будут минимальные.

Так что UB — банально дырка в спецификации и перекладывание ответственности на пользователя.
Не знаю ни одного типа, который вообще выажается терминами «ошибка»
T* и nullptr, pair<Result,Error> и т.п.
Возьмем тот же indexOf, что должно вернуться в случае nilв качестве параметра? Код ошибки? Нет, нужно падать.
Почему? Почему нельзя вернуть invalidIterator?
А теперь та же функция, но возвращает ошибку в дополнение к значению (как чаще всего встречается в Go), тогда каждый второй напишет
Где же тут некорректное поведение? Это зависит от того, что дальше делают с res.
Мы сейчас о корректности, какой смысл иметь быструю программу, которая работает некорректно?
А я не говорил, что нужно избавится от корректности, я просто сказал, что программисту бывает видней, где нужно вставить проверку корректности, внутри цикла или снаружи, например. А дальше компилятор, в идеале, уже должен удостоверится, что где-то проверка есть. Иначе буквально всегда нужно все входные аргументы функций проверять на соответствие области допустимых значений и бросать исключения. Тогда 50% инструкций, будут if.
Что касается производительности, то с range-based циклами можно проверку границ выбрасывать, она гарантированно не выполнится, а для простых обращений — оставлять. Потри быстродействия будут минимальные.
Ну и как же это осуществить, если проверка находится на уровне оператора []?
Так что UB — банально дырка в спецификации и перекладывание ответственности на пользователя.
Нет же. Потому и есть .at() и отдельно []. Хочешь медленно и надёжно — используй .at()
nil это не ошибка, pair<t,u> где оба типа равноценны. Даже тип Exception не имеет «ошибочных» значений, любой объект типа Exception не является ошибочным. Это словоблудие называть обычное значение «ошибкой» и пытаться «ошибку» назвать значением.

Почему нельзя вернуть invalidIterator?

Прекрасный вопрос. Возьмем типовую сигнатуру indexOf: string -> string -> int. То есть два параметра типа string и возвращаемое значение int.
Если мы оставляем ту же сигнатуру и возвращаем invalidIterator, то какое значение он будет иметь? может -2 или -10?

Напомню, что в текущей реализации прекрасно работает код
var index = str.indexOf(x);
var sub = str.substr(x+1, str.length-index-1);

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

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

Я думаю вы уже поняли, что возврат invalidIterator в качестве значения — плохая идея.

Теперь рассмотрим подход Go — меняем сигнатуру функции на string -> string -> (int,error) и берем тот же код, что и выше (только теперь на Go):
index, _ := str.indexOf(nil);
sub := str.substr(x+1, str.length-index-1);

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

Есть третий вариант завернуть ошибку в тип, использовать сигнатуру string -> string -> option int, где option может иметь значения Some(int) и None. Такое используется в ФЯ, но в них есть pattern-matching и параметризованные типы. В Go такого нет и, скорее всего, не будет. Так что можно не рассматривать.

Других вариантов нет. Получается лучший вариант для Go и других императивных языков — кидать исключения. Еще раз повторю: кидать исключения по умолчанию — лучшая стратегия, в отдельных случаях можно возвращать значения, но отдельные случаи надо отдельно рассматривать.

Где же тут некорректное поведение? Это зависит от того, что дальше делают с res.
Как раз не важно, потому что значение некорректное. Любая операция с res будет багом.

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

Ну и как же это осуществить, если проверка находится на уровне оператора []?
В .NET это сделали, причем в самой первой версии. Вы просто мыслите категориями C++.

Нет же. Потому и есть .at() и отдельно []. Хочешь медленно и надёжно — используй .at()
Такой ужас есть только в C++.
nil это не ошибка, pair<t,u> где оба типа равноценны. Даже тип Exception не имеет «ошибочных» значений, любой объект типа Exception не является ошибочным.
Тогда дайте определение, что Вы называете «ошибочным типом», потому что я подразумевал тип, у которого явно некоторые значения несут смысл «нерабочий», например T* может иметь значение nullptr, который явно «ошибка», а вот значение -1 для типа int — это не ошибка, не явно во всяком случае. Тип Error явно сообщает об ошибке, кроме того случая, когда его значение none: Error==none => no Error.
Если мы оставляем ту же сигнатуру и возвращаем invalidIterator, то какое значение он будет иметь?
нечто вроде nullint.
Но что программа может сделать с результатом invalidIterator? Единственный вариант — как-то передать вызывающему методу, что произошла ошибка. Но это тоже самое что делает исключение, причем исключение нельзя проигнорировать, в отличие от «значения».
Статический анализ позволяет не проигнорировать значение-ошибку, точно так же как и выброс исключения, аналогично тому, как компилятор выдаёт warning, когда non-void функция может завершится без return. В идеале, это можно встроить в компилятор — проверку области допустимых значений используемых функции и последующего игнорирования null. Тут с исключениями паритет.
берем тот же код, что и выше (только теперь на Go): Упс, ошибку мы проигнорировали.
Ничего мы не проигнорировали, компилятор выдаст warning: «unused parameter _».
Такое используется в ФЯ, но в них есть pattern-matching и параметризованные типы.
Это развитие идеи, но и без pattern-matching возвращение ошибок через значения не особенно уступают исключениям, при прочих равных.
Как раз не важно, потому что значение некорректное. Любая операция с res будет багом.
ну возможность проверки на nil то должна быть? Имхо это очень неплохой вариант, в таком случае нет необходимости явно бросать исключения, можно всегда возвращать nil в случае ошибки и предоставить пользователю решать, что делать. Если он забудет — рантайм сам выбросит исключение.
Такой ужас есть только в C++.
Почему ужас-то? У вас есть выбор, мы можете использовать .at(), который ведёт себя аналогично другим ЯП, проверяя индекс, или, если вы знаете чего хотите, — используете []. Это гибкость, я тут вообще никаких проблем не вижу. Это уж точно не дыра в спецификации.

Тогда дайте определение, что Вы называете «ошибочным типом»
Это вы придумали термин, вот и объясняйте его.

например T* может иметь значение nullptr, который явно «ошибка», а вот значение -1 для типа int — это не ошибка, не явно во всяком случае

Это зависит от функции. Где-то nil это отсутствие значения, но вполне ожидаемое, а где-то -1 это ошибка, например в функции write. Так что отталкиваться от типов бесполезно. Надо отталкиваться от ожидаемого поведения функции. И когда функция не может уложиться в ожидаемое поведение — кидать исключение (panic).

нечто вроде nullint
Тогда в лучшем случае вы получите NRE, только чуть позже. И какой в этом смысл? Или вы серьезно считаете, что стоит каждый вызвов indexof заворачивать в if?

Статический анализ позволяет не проигнорировать значение-ошибку
Это в теории. А на практике полный статический анализ эквивалентен решению задачи останова (то есть нерешаемо). Статически анализ на сегодня это проверка паттернов в коде, но всегда можно написать код, который пройдет мимо этих паттернов. А в Go для полного счастья еще и defer есть, который усложняет анализ на порядки. Исключения гораздо проще в реализации. Кстати я не уверен, что без явных контрактов можно хоть сколько-нибудь значительное количество проверок сделать в compile-time.

Ничего мы не проигнорировали, компилятор выдаст warning: «unused parameter _».
Вы противоречите сами себе. Warning вообще никого ни к чему не обязывает.

Это развитие идеи, но и без pattern-matching возвращение ошибок через значения не особенно уступают исключениям, при прочих равных.
Не понял на основании чего вывод. Вы могу не привели ни одного аргумента в пользу значений. Пока есть отдельные сценарии где ошибки в теории, при наличии проверок в compile-time могут быть не хуже исключений. Это очень далеко от реальности.

ну возможность проверки на nil то должна быть?
Кому должна? Зачем проверка? Что вы по результатам проверки сделаете?
Если он забудет — рантайм сам выбросит исключение.
Только исключение будет NRE, а не FileNotFound и потеряется stacktrace. так что возврат nil хуже в этом плане без каких-либо преимуществ в других местах.

Почему ужас-то?
В других языках такого нет. Отсутствие проверок в [] нужно только в vector и только в циклах. Но в циклах можно использовать итераторы и вообще не обращаться к [].
Нужен такой оператор только для одного, Когда создавали C++ возможно был в этом смысл, но в 2015 году зачем такое надо?

И авторы языка это понимают. Проверок по факту нет только в release билде с отключенным флагом secure_чтототам.

Все разговоры про гибкость это плач в пользу бедных.
UFO just landed and posted this here
Мы сейчас о корректности, какой смысл иметь быструю программу, которая работает некорректно? Тогда проще на ассемблере или голом C писать, точно ни одной лишней операции не будет.

А это должен решать менеджмент в идеале.
UFO just landed and posted this here
Тем не менее в большинстве языков есть nil, но они умудряются делать надежные системы обработки ошибок.
UFO just landed and posted this here
Когда программа не выдаст некорректный результат в случае ошибки если не делать ничего дополнительно.
UFO just landed and posted this here
Если ошибки выражены эксепшнами, то вся программа просто упадёт. На это есть гарантия.
UFO just landed and posted this here
Почитай ветку выше. Исключительность ситуации — относительное понятие. Где-то неверный формат — исключительная ситуация, а в другом месте лишь валидация вводимых данных. Поэтому нет смысла классифицировать ошибки по причине возникновения.

По факту уместна лишь одна классификация:
1) логические ошибки — которые говорят о том, что в программе баг. Например NRE, ArgumentException. InvalidOperationException итд.
2) runtime ошибки — которые возникают в процессе работы программы и зависят от факторов внешних по отношению к программе.

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

Runtime можно ловить, а можно и не ловить. Это полностью зависит от логики программы. В Go решили построить runtime ошибки на возврате дополнительного значения. То есть нужно явно обрабатывать эти значения и за этим почти никто не следит (ворнинги компилятора не в счет). И это решение могло бы быть правильным, если бы не одно обстоятельство — в подавляющем большинстве случаев программа ничего не сможет сделать с runtime ошибкой.

Так что возврат ошибки в качестве значения, особенно в методах read и write (на примере которых мы это все рассматриваем) — очень плохая идея. Выше я описал разные стратегии работы с ошибками и подход Go проигрывает по всем параметрам.

Так что вам кажется не соответствует действительности. Для доказательства посчитай сколько «обработок ошибок» в Go занимаются лишь пробрасыванием ошибки вверх по стеку вызовов.
UFO just landed and posted this here
Однако, экзепшоны тоже плохо — их повальное использование превращает код в кашу из goto XXI-го века, да и сами по себе они обходят систему типов.
А не могли бы вы добавить конкретики? Я вот вижу что неправильное использование Exceptions превращает код в кашу. Но так можно сказать про что угодно. В нормальном случае код с исключениями гораздо проще, чем с ручной обработкой ошибок.

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

Монадки-монадочки, это всё.

1) Error monad эквивалентен try\catch\finally. Более того, их можно автоматически переписать друг в друга. Так что никаких преимуществ перед исключениями нет, кроме функционального стиля.
2) Error monad породит такую же иерархию типов-исключений
3) точно также по сигнатуре функции нельзя будет определить какой тип исключения выбрасывает функция
4) Исключения точно также пролетают наверх до того уровня, где появилось желание их обработать.
5) С монадами компилятор точно также ничего не может гарантировать

Непонятно почему вы ругаете исключения, и хвалите их 100%-й эквивалент.
UFO just landed and posted this here
Гм. Этот ErrorType, он не делает ли тоже самое, что checked exceptions? Не получается так, что при необходимости сменить ErrorType, приходится менять сигнатуры всего, чего коснулась функция?
UFO just landed and posted this here
если бы можно было делать переменные для списка экзепшонов

Такую роль выполняют базовые классы. Не совсем то, но близко.
UFO just landed and posted this here
Это когда всё на экзепшонах.
А где вы такой код видели?
Моя очередь просить конкретики. Что за нормальный случай и что за ручная обработка ошибок?
Нормальный случай — когда перехватываются только ошибки, которые можно обработать, когда не используются exceptions для валидации. Ручная обработка — как в Go, пока руками код не напишешь ошибка не обрабатывается никак.

Если у меня функция возвращает Either ErrorType Value, я могу посмотреть на определение ErrorType, могу посмотреть, какие ошибки там предусмотрены, могу посмотреть, где этот Either разворачивается, и так далее.

У тебя будут разные несвязанные ErrorType на каждый вид ошибки? Тогда замучаешься писать сигнатуры и, самое проблемное — пропадет структурированная обработка ошибок. Ты не сможешь перехватить общий тип и залогировать например.
Если же ты выстраиваешь ErrorType в иерархию, то получатся те же эксепшены, вид сбоку.

Далее еще интереснее. Предположим у тебя есть функция, которая возвращает Either ErrorType1 T, а параметр этой функции — лямбда. И внезапно ты передаешь лямбду, которая возвращает Either ErrorType2 T. В этом случае программа скомпилируется? Если да, то что будет когда вылетит ErrorType2? Эта также проблема, что возникла с checked exceptions в Java.

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

Я, видимо, что-то не знаю либо про error monad, либо про try/catch/finally. Почему это они эквивалентны?
Потому что реально есть алгоритм, переписывающий одно в другое и наоборот. Это и есть доказательство эквивалентности.
Я говорю про штатный метод обработки. Понятно что в любом языке с генериками можно Try монаду слепить. Scala всетаки на Java работает, а у нее основной метод — исключения для рантайм ошибок.
UFO just landed and posted this here
Ну это даёт гарантию, что при возникновении ошибки всё упадёт.
UFO just landed and posted this here
Оговаривалось, что такая гарантия есть, если программист не ломает её самостоятельно.
UFO just landed and posted this here
В Go оно изначально поломано, даже писать дополнительно ничего не надо.
UFO just landed and posted this here
Одна из основных причин возникновения исключений — высокая вероятность просто проигнорировать «нефатальную ошибку», то есть ту которую можно обработать. Причем игнор, когда мы возвращаем ошибки как значение — действие по-умолчанию, от этого хреново вдвойне. В Go нет механизма, позволяющего по умолчанию не игнорировать ошибку, кроме panic.
Давайте говорить в контексте статьи. Возьмите ваше утверждение, и замените «ошибку» на «переменную». Почему вам не хочется возмущаться, что в языке нет механизма проигнорировать возвращаемую переменную? Поймите, в Go:
state := foo.State()

и
err := foo.CheckUser()

это одинаковые вещи, вы работаете с ними теми же средствами языка.

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

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

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

Такая философия верна для 99,99% случаев. Оставшийся 0,01% закрывается просто другим API, который явно возвращает ошибку, а еще лучше «статус операции».

Почему-то создатели Go решили, что эти 0,01% случаев так важны, что надо непременно стандартную библиотеку построить на явном возврате ошибок. Хотя я уверен что никто из присутствующих ни разу не пытался что-то сделать с ошибками функции write.
Ошибка мне не нужна, это досадное недоразумение,

Отлично. Вы прекрасно демонстрируете то, с чем борется Go — с отношением к ошибкам, как к чему-то лишнему, как к недоразумению. И подход «ошибки это значения» заставляет вас так не относиться. Вы лучше всего продемонстрировали суть статьи :)

Хотя я уверен что никто из присутствующих ни разу не пытался что-то сделать с ошибками функции write.

*Всегда* проверяю ошибку после Write. Большинство знакомых мне Go-программистов — тоже. Непонятно, откуда у вас такая уверенность, особенно когда целые статьи пишут, с объяснением о том, почему в Go «не проверять» ошибки без повода — это редкость и моветон.
Отлично. Вы прекрасно демонстрируете то, с чем борется Go — с отношением к ошибкам, как к чему-то лишнему, как к недоразумению. И подход «ошибки это значения» заставляет вас так не относиться. Вы лучше всего продемонстрировали суть статьи :)
Это все философия, в коде ничего лучше от нее не становится.

*Всегда* проверяю ошибку после Write. Большинство знакомых мне Go-программистов — тоже.

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

*Всегда* проверяю ошибку после Write

И какие действия выполняете, если ошибка обнаружена? Кстати, проверяете сам факт ошибки, или для каждого кейса ветку делаете?
В зависимости от задачи. Вы, кажется, так и не поняли, о чем речь.
Мне ваш вопрос звучит так:
«И какие вы действия делаете, когда получаете корень квадратный из функции, которая считает корень квадратный? Кстати, разбираете ли сам факт корня, или для каждого кейса делаете ветку?»
Глупо звучит, согласитесь)
Нет, он звучит как «и какие действия вы делаете, когда получаете (не важно как) не посчитанный корень квадратный, а признак того, что посчитать его невозможно? Кстати, разбираете ли сам факт невозможности посчитать корень, или для каждого кейса делаете ветку?»
Нет, он звучит как

Давайте заканчивать эту «дискуссию». Я вам пишу как мне звучат ваши слова, а вы мне «нет, вам они звучат не так».
Ну и суть статьи вы так и уловили — хотя в этом, конечно же, и моя вина, как автора статьи.
А можно ссылку на статью с объяснением о том, почему в Go «не проверять» ошибки без повода — это редкость и моветон? Из вашей статьи напрашивается вывод, что единственный повод так делать — культура, сформированная в сообществе Go.
Я уже писал такую статью тут на Хабре, но, скорее всего, я сам не знаю всех причин. Понимаете, как выходит — я больше практик, чем теоретик. Go достаточно сильно повлиял на моё отношение к ошибкам, тут и язык, и культура, и статьи на эту тему, и код, который я вижу у других — все вместе. То, что вижу на практике, подтверждается словами массы других людей, которые говорят тоже самое.
Поэтому если вы только ищете «формальных математических доказательств», почему так происходит — и все мои аргументы отметаете, потому что «там тоже можно проигнорировать» — боюсь, я вам не помогу.
Может быть, если бы вы сами стали практиком, и пописали немного на Go (не одну недельку, всмысле), вы бы нашли более правильное и емкое объяснение, почему Go производит такой эффект.
Но мне, все же, более важно, что это так на практике, даже если я это не могу донести «теорией» :)
А что мешает мне сделать
try {
//...
} catch (Throwable ignore) {
}
? И вы знаете я такой код видел вполне в рабочих библиотеках и проектах.
Ничего не мешает. Но сравните что надо сделать в Go для такого же эффекта:
//...

По факту в Go ничего не надо делать для игнора ошибки. Максимум написать "_" вместо имени переменной.

Дальше еще интереснее. Я просто написал аналитику для компилятора C#, которая находит пустые catch и catch с очень общим типом исключения. Поэтому у меня на такой код компилятор ругается при сборке.

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

А если вы решили игнорировать ошибку, то вас ничто не остановит. Ну только ваши коллеги.
Как он проверяет что ошибка не игнорируется? Очень просто ведь сохранить ошибку в переменную и не делать ничего с ней. Линтер это проверяет? Уверен что нет. А в случае использования класса Scanner как проверят что ошибка не проигнорирована?
UFO just landed and posted this here
Да. Там простое правило: если очень общий тип исключения ловится и внтури catch нету оператора throw — это проблема. На пустой catch ругается независимо от типа исключения.
Эту проблему решили async/await в том же C#, которые решают проблему асинхронного программирования куда более изящно, нежели обычные такие fiber threads горутины с синтаксическим сахаром. Все исключения отлично пробрасываются туда, куда нужно и сохраняют правильный callstack. Даже если идет речь о чем-то вроде горутин, когда код отправляется на ThreadPool. Go естественно слишком прост, чтобы еще в нем модель async/await реализовывать.
А какие ещё бывают ошибки, кроме исключений и значений?
Это не совсем то, по ссылке приводится классификация ошибок по причине их возникновения. Я пытаюсь разобраться в сути фразы «ошибки это значения». Почему ошибки являются значениями в Go, мы тут кое-как разобрались. Почему они не являются исключениями в Go тоже понятно — исключений просто нет в этом языке. Отсюда вопрос — а чем ещё могут быть ошибки, кроме как значениями и исключениями?
В Go есть panic, который по механике похож на исключения.
Действием — лямдой, например. Что близко к исключениям, но не является ими. В чистом виде слабоприменимо и требует странных умений от языка программирования, хотя, концептуально, нечто подобное, похоже, периодически применяется для выбрасывания исключений для различных ко-ротин, тасков и прочего (где в случае ошибки, нужно выполнить «чужой» код в своём контексте и упаковать некоторое текущее состояние в понятную для запускающего контекста форму).
В чистом виде слабоприменимо

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

n = write(“one”)



Надо так:
n = write("one")


Это не для красоты, а для возможности скопировать код и запустить сразу.
Оу, спасибо. Это при копировании в/с медиума поменялось.
Для возможности запустить код — лучше переходите в песочницу, там под каждым сниппетом ссылки — там полная версия кода.
Бред (лат. Delusio) часто определяют как расстройство мышления с возникновением не соответствующих реальности болезненных представлений, рассуждений и выводов[1], в которых больной полностью, непоколебимо убеждён и которые не поддаются коррекции[2].
Ни в коем случае не хочу никого назвать больным, но кое-кто в этом и нескольких других тредов ведёт себя не очень адекватно, спорит с очевидными истинами, а также ссылается на свой опыт и авторитет Роба Пайка и команды (в науке, в т.ч. компьютерной, нет места авторитетам), пытаясь таким образом опровергнуть даже самые логичные и обоснованные аргументы против конкретных решений в проектировании Go, и в целом создаёт себе репутацию то ли сектанта, то ли тролля. Но я не буду показывать пальцем, т.к. хочу не обидеть этого человека, а заставить посмотреть на себя со стороны и задуматься.
Спасибо за урок воспитания и аргументацию в стиле «спорит с очевидными истинами», отвечу вам просто. Когда два человека сравнивают А и Б, и один из них не пробовал Б, но доказывает, что Б хуже, потому что он привык к А — он заведомо неправ. Вот и мне хотелось бы от некоторых яростных комментаторов тут, чтобы вы строили свои мнения объективно, а не прикрывались отсутствием знаний и «очевидными истинами».

Как мне кажется в данной публикации https://habr.com/ru/post/517436/ отчасти есть некоторые ответы, на вопросы упомянутые в комментариях.

Sign up to leave a comment.

Articles