Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
Да, в Java еще 20 лет назад осилили единый механизм обработки исключений.
могли бы возвращать из методов Pair<Result, Exception>
Да, checked exceptions не дружат с лямбдами. Но разве это значит, что весь механизм обработки исключений не работает с лямбдами?
Что за статья и почему не решение? Напрямую с потоками в джаве мало кто работает. В современных фреймворках типа Akka, проблем с обработкой исключений нет.
Т.е. если что-то из перечисленного появится, пара результат-исключение станет более предпочтительна, чем try-catch?
Неужели? Почему же тогда в комментарии, с которого началась эта дискуссия автор радостно объясняет нам важность такого разделения, да еще как будто в го первыми до него додумались.
Получается, мало того, что идея далеко не нова, так она еще и успела провалиться много лет назад. Зачем же это затаскивают в го? Наступают на те же грабли?
Вернемся к Go. Он идеологически разделяет подход к работе с этими сущностями. Для предвиденных ошибок — это возвращаемое значение, это такой же результат работы процедуры и вы должны его обрабатывать (вы же не игнорируете результат, который возвращает процедура поиска первого вхождения подстроки в строке, даже если это "-1"?). Для непредвиденных исключительных ситуаций — это defer, panic и recover.
C++, C#, Java и др. идеологически используют единый подход для работы с ними
А defer, внезапно, это эмуляция finally/деструкторов С++ на стеке. Ни больше, ни меньше.
Почитайте про парсеры. И это таки видится более частным применением, нежели по прямому значению — завалить программу.
может произойти ошибка: файл не существует, недостаточно прав и т.д.
Возьмем теперь непредвиденную исключительную ситуацию: разыменование нулевого указателя.Всё таки не совсем понятно, как вы провели границу между «предвиденной ошибкой» и «непредвиденной исключительной ситуацией». Ведь всегда можно сделать
if(ptr==nullptr) {
printf("error X");
}
Почему же нельзя предусмотреть подобную ошибку?После такой ошибки практически невозможно восстановить нормальное поведение программыКак и после любой ошибки, если она не была обработана, например почти нельзя восстановить нормальное поведение после попытки записи в несуществующий файл.
Если я читаю 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 должно также поддерживать установку описания ошибки. Если следовать Вашей же логике, как я понял.
Код обработки ошибки — это не особенная конструкция, это полноценная часть вашего кода, как и все остальные… Ещё раз — ошибки это обычные значения, и обработка ошибок — это такое же обычное программирование.
Те, кто писал более-менее сложный код на С (в ядре ОС, к примеру), обычно очень хорошо понимают весь ужас C-style error handling.
Это все тот же механизм ошибка=значение.
Можно себя долго обманывать и искать в Go новизну, но как уже множество раз говорилось авторами языка — в нем нет ничего нового.
Ясно.Очень приятно, что вы наконец осознали, что привели пример, в котором проигнорировать ошибку просто.
Хоть лбом об стенку бейся ))Пожалуйста, не бейтесь головой об стену, это не принесёт пользы никому. Лучше напишите статью, которая продемонтстрирует каким образом в Go можно сделать игнорирование проверки ошибки сложной задачей. Также пожалуйста раскройте подробнее в каких случаях применяется подход подобный приведённому в вашей текущей статье, а в каких случаях тот, который усложняет игнорирование ошибок.
do {
....
if (error) {
break;
}
....
if (error) {
break;
}
....
} while(false);
/* Release resources here. */
....
но подход Go тут является и зрелым и свежим одновременно
Go просит программиста относиться к ошибкам также, как и к любому другому коду. Просит не рассматривать ошибки, как некоторую особую сущность, для которой нужен специальный подход/инструмент/паттерн, а трактовать код обработки ошибок также, как бы вы трактовали обычную логику вашей программы. Код обработки ошибки — это не особенная конструкция, это полноценная часть вашего кода, как и все остальные.
Смотрим на С и, внезапно, видим ровно тоже самое.
большинстве случаев обработка ошибок это скорее вспомогательная часть на черный день, которая размывает основную логику
Исключения позволяют отделить обработку того, что к логике мало относится — оно отделяет обработку исключительной ситуации
к коду в котором нормальной обработки ошибок нет, как понятия
Ну и это, кажется вы прекратили со мной говорить во всех будущих темах. Что же случилось?
Я не вижу смысла общаться с человеком
который так возносит себя над всеми остальными, включая пионеров computer science, унижает их и новые для себя технологии, лишь из-за того, что привык работать с другими технологиями.
// До
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
}
fmt.Errorf(«User is not registered»)
E.BS(
E.DS(
fmt.Errorf(«User can't send messages»)
Если бы я был королем, я бы заставил всех авторов библиотек следовать двум правилам:
— errors.New() использовать только для объявления новых публичных переменных (типа var ErrNoRows ...)
— если вам таки нужно использовать fmt.Errorf() — не используйте, а сделайте кастомный тип для этой ошибки
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
}
Тогда для чего это называется «ошибкой»?
обработка ошибок в Go сделана примерно как в C, только слегка удобнее.
Можно было бы применить монады
Получился в итоге язык, который был бы очень востребован 25 лет назад.
Нет, между Go и C огромные отличия, вы же видите только схожести и делаете выводы только из них.
В Go нет монад.
>> Получился в итоге язык, который был бы очень востребован 25 лет назад.
SpaceX, Dropbox, Cloudflare и много других взрослых компаний с вами не согласны.
Нет, между Go и C огромные отличия, вы же видите только схожести и делаете выводы только из них.
фатальных и около-фатальных ошибкахЧто имеется ввиду? Например?
if (db.ok()==false) {
printf("db is dead");
return false;
}
if (remote.fail()) {
rollback();
} «Скакануло напряжение» — это не ошибка. Программа не может знать о «скачке напряжения», только о статусе датчиков, ParseError или «Segmentation fault». На статус датчиков и ParseError можно отреагировать. А на «Segmentation fault» действительно лучше падать, но тут и делать ничего не надо. Я веду к тому, что нет таких случаев, когда нельзя выброс исключения заменить на «адекватно среагировать». if (db.ok()) {
db.read(...)
}
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 { ...}
«ошибка» не является внешней, то есть может быть формально исключена на стадии разработкиЯ не понял, что Вы хотели сказать. В случае indexOf, "-1" как раз является дополнительным значением ошибки таким же как nullptr. Если программист не вставит проверку на -1, можно получить UB.
Только в этих условиях можно возвращать значение вместо ошибки. Причем именно значение как результат функции, а не как дополнительное значение «ошибки».
Я не понял, что Вы хотели сказать. В случае indexOf, "-1" как раз является дополнительным значением ошибки таким же как nullptr.
Если программист не вставит проверку на -1, можно получить UB.UB это термин из мира C++, там много дырок в спецификации. В большинстве языков отрицательные индексы массива явно запрещены, поэтому отсутствие проверки на -1 приведет к index out of range exception в следующей строке.
var index = str.indexOf(x);
var sub = str.substr(x+1, str.length-index-1);
«Фатальность» ошибки — вещь относительная.К тому же, согласно вашему предыдущему посту, фатальность не является критерием для выбора между ошибкой и исключением. Вы предлагаете использовать в качестве критерия свойства возвращаемого типа. Если тип явно предусматривает значение-ошибку возвращать ошибку, иначе — исключение.
Причем «ошибка» является одни из значений функции, а не дополнительным. Так сложнее игнорировать.Почему сложнее? Наоборот даже. Что Вы имеете ввиду под «одним из значений функции, а не дополнительным»? Что возвращается тот же самый тип, что и обычно, но значение сигнализирует об ошибке (как nullptr)? Альтернатива — возвращать другой тип или пару?
B это термин из мира C++, там много дырок в спецификации. В большинстве языков отрицательные индексы массива явно запрещены, поэтому отсутствие проверки на -1 приведет к index out of range exception в следующей строке.За это приходится платить производительностью. UB — это не дыры в спецификации, а сознательный выбор обусловленный тем, что для выброса исключения нужно проверять аргумент, то есть вставлять дополнительный if (index<0) {throw;}, что не всегда необходимо на самом деле т.к. во многих случаях эту проверку можно вынести на более высокий уровень, за пределы цикла, например. Для решения этой проблемы существует assert(), который проверяет индекс в debug версии, но не делает ничего в release. Имхо, это решение лучше, чем перманентный неотключаемый debug в тех языках, где выбрасывается исключение «index out of range».
Если тип явно предусматривает значение-ошибку возвращать ошибку, иначе — исключение.Не знаю ни одного типа, который вообще выажается терминами «ошибка». Если делается новый тип, то любое его значение корректно.
Почему сложнее? Наоборот даже.
res, _ := indexOf(...)
За это приходится платить производительностью.Мы сейчас о корректности, какой смысл иметь быструю программу, которая работает некорректно? Тогда проще на ассемблере или голом C писать, точно ни одной лишней операции не будет.
Не знаю ни одного типа, который вообще выажается терминами «ошибка»T* и nullptr, pair<Result,Error> и т.п.
Возьмем тот же indexOf, что должно вернуться в случае nilв качестве параметра? Код ошибки? Нет, нужно падать.Почему? Почему нельзя вернуть invalidIterator?
А теперь та же функция, но возвращает ошибку в дополнение к значению (как чаще всего встречается в Go), тогда каждый второй напишетГде же тут некорректное поведение? Это зависит от того, что дальше делают с res.
Мы сейчас о корректности, какой смысл иметь быструю программу, которая работает некорректно?А я не говорил, что нужно избавится от корректности, я просто сказал, что программисту бывает видней, где нужно вставить проверку корректности, внутри цикла или снаружи, например. А дальше компилятор, в идеале, уже должен удостоверится, что где-то проверка есть. Иначе буквально всегда нужно все входные аргументы функций проверять на соответствие области допустимых значений и бросать исключения. Тогда 50% инструкций, будут if.
Что касается производительности, то с range-based циклами можно проверку границ выбрасывать, она гарантированно не выполнится, а для простых обращений — оставлять. Потри быстродействия будут минимальные.Ну и как же это осуществить, если проверка находится на уровне оператора []?
Так что UB — банально дырка в спецификации и перекладывание ответственности на пользователя.Нет же. Потому и есть .at() и отдельно []. Хочешь медленно и надёжно — используй .at()
Почему нельзя вернуть invalidIterator?
var index = str.indexOf(x);
var sub = str.substr(x+1, str.length-index-1);
index, _ := str.indexOf(nil);
sub := str.substr(x+1, str.length-index-1);
Где же тут некорректное поведение? Это зависит от того, что дальше делают с 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 — это не ошибка, не явно во всяком случае
нечто вроде nullintТогда в лучшем случае вы получите NRE, только чуть позже. И какой в этом смысл? Или вы серьезно считаете, что стоит каждый вызвов indexof заворачивать в if?
Статический анализ позволяет не проигнорировать значение-ошибкуЭто в теории. А на практике полный статический анализ эквивалентен решению задачи останова (то есть нерешаемо). Статически анализ на сегодня это проверка паттернов в коде, но всегда можно написать код, который пройдет мимо этих паттернов. А в Go для полного счастья еще и defer есть, который усложняет анализ на порядки. Исключения гораздо проще в реализации. Кстати я не уверен, что без явных контрактов можно хоть сколько-нибудь значительное количество проверок сделать в compile-time.
Ничего мы не проигнорировали, компилятор выдаст warning: «unused parameter _».Вы противоречите сами себе. Warning вообще никого ни к чему не обязывает.
Это развитие идеи, но и без pattern-matching возвращение ошибок через значения не особенно уступают исключениям, при прочих равных.Не понял на основании чего вывод. Вы могу не привели ни одного аргумента в пользу значений. Пока есть отдельные сценарии где ошибки в теории, при наличии проверок в compile-time могут быть не хуже исключений. Это очень далеко от реальности.
ну возможность проверки на nil то должна быть?Кому должна? Зачем проверка? Что вы по результатам проверки сделаете?
Если он забудет — рантайм сам выбросит исключение.Только исключение будет NRE, а не FileNotFound и потеряется stacktrace. так что возврат nil хуже в этом плане без каких-либо преимуществ в других местах.
Почему ужас-то?В других языках такого нет. Отсутствие проверок в [] нужно только в vector и только в циклах. Но в циклах можно использовать итераторы и вообще не обращаться к [].
Мы сейчас о корректности, какой смысл иметь быструю программу, которая работает некорректно? Тогда проще на ассемблере или голом C писать, точно ни одной лишней операции не будет.
Однако, экзепшоны тоже плохо — их повальное использование превращает код в кашу из goto XXI-го века, да и сами по себе они обходят систему типов.А не могли бы вы добавить конкретики? Я вот вижу что неправильное использование Exceptions превращает код в кашу. Но так можно сказать про что угодно. В нормальном случае код с исключениями гораздо проще, чем с ручной обработкой ошибок.
Нельзя взять и посмотреть на сигнатуру функции, чтобы понять, что она там делает, кидает, и как это обрабатывает. Соответственно, и компилятор не может ничего гарантировать касательно свойств обработки этих ошибок.Это верно для любой обработки ошибок.
Монадки-монадочки, это всё.
Это когда всё на экзепшонах.А где вы такой код видели?
Моя очередь просить конкретики. Что за нормальный случай и что за ручная обработка ошибок?Нормальный случай — когда перехватываются только ошибки, которые можно обработать, когда не используются exceptions для валидации. Ручная обработка — как в Go, пока руками код не напишешь ошибка не обрабатывается никак.
Если у меня функция возвращает Either ErrorType Value, я могу посмотреть на определение ErrorType, могу посмотреть, какие ошибки там предусмотрены, могу посмотреть, где этот Either разворачивается, и так далее.
Я, видимо, что-то не знаю либо про error monad, либо про try/catch/finally. Почему это они эквивалентны?Потому что реально есть алгоритм, переписывающий одно в другое и наоборот. Это и есть доказательство эквивалентности.
Так что из нескольких десятков языков, которые живы на сегодня, никто такой подход не использует, каким бы красивым он не казался.
state := foo.State()err := foo.CheckUser()Ошибка мне не нужна, это досадное недоразумение,
Хотя я уверен что никто из присутствующих ни разу не пытался что-то сделать с ошибками функции write.
Отлично. Вы прекрасно демонстрируете то, с чем борется Go — с отношением к ошибкам, как к чему-то лишнему, как к недоразумению. И подход «ошибки это значения» заставляет вас так не относиться. Вы лучше всего продемонстрировали суть статьи :)Это все философия, в коде ничего лучше от нее не становится.
*Всегда* проверяю ошибку после Write. Большинство знакомых мне Go-программистов — тоже.
*Всегда* проверяю ошибку после Write
try {
//...
} catch (Throwable ignore) {
}
? И вы знаете я такой код видел вполне в рабочих библиотеках и проектах.//...
n = write(“one”)
n = write("one")
Бред (лат. Delusio) часто определяют как расстройство мышления с возникновением не соответствующих реальности болезненных представлений, рассуждений и выводов[1], в которых больной полностью, непоколебимо убеждён и которые не поддаются коррекции[2].Ни в коем случае не хочу никого назвать больным, но кое-кто в этом и нескольких других тредов ведёт себя не очень адекватно, спорит с очевидными истинами, а также ссылается на свой опыт и авторитет Роба Пайка и команды (в науке, в т.ч. компьютерной, нет места авторитетам), пытаясь таким образом опровергнуть даже самые логичные и обоснованные аргументы против конкретных решений в проектировании Go, и в целом создаёт себе репутацию то ли сектанта, то ли тролля. Но я не буду показывать пальцем, т.к. хочу не обидеть этого человека, а заставить посмотреть на себя со стороны и задуматься.
Как мне кажется в данной публикации https://habr.com/ru/post/517436/ отчасти есть некоторые ответы, на вопросы упомянутые в комментариях.
Почему «ошибки это значения» в Go