Comments 78
Согласен, публиковать статьи, которые идут вразрез с популярными мнениями, требует смелости.)
Выбор между исключениями и обработкой ошибок зависит от контекста, языка программирования и предпочтений разработчика. Оба подхода имеют свои плюсы и минусы, и важно понимать, когда какой из них лучше использовать.
>Исключения могут быть выражены через обработку ошибок, а обработка ошибок через исключения. То есть тезисы о скорости, перфомансе и прочем неактуальны
Каким они могут быть неактуальны, если исключения всегда работают хуже (хотя бы потому что сохраняют стек вызовов (забудем про C++)). А вообще - исключения частный случай алгебраических эффектов (см. язык Koka).
>При этом, исключения дают куда более лаконичный код.
До тех пор пока не нужно их обрабатывать (try catch finally не вершина лаконичности). И все это ценой отсутствия типа ошибок в сигнатуре функций (забудем про Java и checked exceptions, которые почему-то все ругают) - пишите в каждой функции try или все может упасть в неподходящий момент.
>Дополнительно, исключения дают некоторые инструменты, которые при возврате ошибок недоступны: например при переходе к ошибкам утрачивается возможность распечатки стека вызовов
Исключения и стек вызовов - вещи ортогональные (тут пора вспомнить про C++, где стека нет).
В расте можно включить стек вызовов через RUST_BACKTRACE=1.
fn main() {
foo();
}
fn foo() {
bar();
}
fn bar() {
panic!("oops");
}

Вообще стеки вызовов удобны для программиста, но не удобны для пользователя - можно создавать собственные стеки ошибок (добавлять контекст), например через anyhow https://crates.io/crates/anyhow. В golang практикуются похожие вещи, и все счастливы.
все эти алгебраические эффекты - это попытка справиться с кандалами, которые сами на себя надели: смешивать ошибки с возвращаемыми значениями банально неудобно, противоестественно.
Алгебраические эффекты - это не (только) про ошибки - это попытка объединить на первый взгляд разные вещи: исключения, генераторы, корутины (прерываемые функции) и т.п. Потому что с монадами и и прочими есть некоторые проблемы, но пока это на уровне исследований.
Не знаю зачем вы начали говорить про мутабельность. В мутабельности на уровне функции я не вижу ничего плохого, если функция остается чистой. В условиях многопоточности - функции без общего мутабельного состояния проще для использования и понимания, исключает ряд ошибок и позволяет не использовать мьютексы и т.п. Erlang вполне себе хорошо живет в многопоточном мире без изменяемых данных.
Тем временем в реальном мире немутабельные программы давно выиграли у мутабельных.
Т.к. практически все современные СУБД строятся на иммутабельных структурах. Даже там где есть мутабельность (postgresql) - это изменение пару бит, реально данные не меняются и там.
Большая часть современных программ - вообще stateless (особенно тех, что пишутся на го).
Неопсредственная мутабельность - нужна в ограниченном числе случаев и почти всегда приводит либо к гонке, либо к сбросу кеша процессора (привет секте любителей гошных указателей).
Вы когда-нибудь слышали про принцип "Функциональное ядро - Императивная оболочка". Очевидно для того, чтобы взаимодействовать с окружением нужно работать с ним как с глобальной изменяемой "переменной". Однако бизнес-логика может быть и должна быть изолирована от окружения насколько это возможно. Чистые функции проще тестировать и понять, а любым изменяемым состоянием нужно управлять.
Мир меняется, но некоторые вещи не меняются, например законы геометрии - там нет состояния - есть входные данные (стороны треугольника равны), по которым можно получить другие данные (все углы равны), используя ограниченный набор аксиом - базовых функций в каком-то смысле.
В го нынче на уровень линтеров (кодстайла) вытащили требование не только разворачивать стек при ошибки в ручную (return err), но ещё и содержать почти полный стек (fmt.Errorf("kokoko: %w", err).
Насчёт все счастливы, интересное утверждение. Учитывая что proposal's с аналогичным растовому синтаксису "?" были весьма популярны.
это конечно прекрасно, но в итоге в го нет ни одного стека вообще. Т.к. враппится только один. И ошибка - не исключение, это не упавший код.
Это если его перехватили. Я буквально вчера на ревью человека попросил кетчер в первой строке короутины написать в дефёре.
И ответ "это вопрос чистоплотности" не принимаю, т.к. 99% разработчиков не способны написать больше 100 строк кода без бага.
Разговор глухого со слепым. Вы моими же аргументами о некомпетентности разработчиков, пытаетесь оправдать некомпетентность.
В го есть строгое соглашение на распостранение паники. Можете продолжать вашу борьбу, но рекомендую вам перейти на котлин. Там ваши проблемы решены и даже нетив есть. Правда работает медленее, чем го. Пока медлнее.
А я пожалуй буду таков.
Вы упомянули Koka. Вы его уже для чего-нибудь применяли? Есть впечатления от практического использования?
Этак в скорости и до монад доберётесь: https://habr.com/ru/articles/339606/
Если даже в стандартной библиотеке разночтения, то на что опираться, выбирая сигнатуры для своих функций?
func ParseIP(s string) IP
, но func ParseMAC(s string) (hw HardwareAddr, err error)
Ну как так-то.
Ну как так-то
Потому что обратная совместимость
Идея Must правильная, реализация - нет. panic не для этого.
Правильная реализация должна работать как возвращение ошибки, но язык такой конструкции не даёт, к сожалению. Плюс возникают вопрос, как быть с разным количеством аргументов. Получается у каждого аргумента должно быть возвращено дефолтное значение.
Замена ошибок на панику? Поздравляю, вы испортили обработку ошибок. Паники используются в исключительных случаях, ошибки в 90% нужно возвращать наверх.
Не в первый раз такое вижу, каким образом условная конструкция(if) затрудняет чтение? Несколько линейных if'ов теперь начали путать людей? Есть так, то проблема скорее в программисте.
Многословно - да, не читаемо - ни в коем случае.
Поражает упорство с которым люди пытаются придумать велосипед поверх предельной простой системы обработки ошибок, ну нет тут '?' из раста, ну смиритесь, либо поменяйте язык, если if err != nil вас так корёжит.
Всем нужно, никто не будет доволен если библиотека внутри себя вызовет нежданную панику.
Да, чаще всего именно это и нужно, похоже это на исключения или нет - роли не играет.
и получается, Вы тоже пришли к поздравлению:
Ни коим образом, я предлагаю использовать ошибки так, как их задумывали авторы языка. Автор статьи лепит велосипед, пытаясь превратить язык в, знакомый ему, javascript/python.
а если она выдаст задокументированную панику, то всё ok.
Да, это ок, если произошло то, из-за чего библиотека не может функционировать, и это задокументировано - паника ожидаема.
мы обсуждаем вопрос, что авторы языка могут ошибаться и ошиблись.
Авторы языка не ошиблись с дизайном, он простой и лаконичный. Было несколько недочётов, обработка ошибок - не один из них.
В которых неудобный err и defer, заменится на удобный try/catch/finally/throw
Просто смешно, все поняли что исключения - неправильный способ работы с ошибками, все современные языки уходят от него, и это прекрасно. И вообще с каких пор defer плох? Невозможно воспринимать такую позицию, как адекватную.
От адептов исключений всегда одна и та же ошибка: код, который ты привел в пример, не делает тоже самое, что и if err!=nil. Попробуй отлавливать исключения(паники) индивидуально на каждую функцию, я посмотрю что получится.
И есть втрое более сложный вариант с if err != nil после каждого вызова функции.
Если 3 if - это сложно, то мои глубочайшие соболезнования.
Авторы ХОТЕЛИ чтобы язык имел простой и лаконичный дизайн. Однако авторы попали в ловушку
Никто никуда не попал, все довольны и с радостью пользуются одной из самых понятных систем работы с ошибками. И только новички, привыкшие к ужасам js/Java/python поначалу трясутся от "ошибок как значений" и отсутствия сахара.
да, выводят за пределы экрана.
нет, усложнение не «втрое»
Я поймал себя на том, что, читая код, пропустил все if err != nil.
Да, читать менее удобно, и в один glimpse код не помещается. Но разница не в три, и даже не в два раза. Максимум полтора, а то и процентов 10-15%.
3 if умножают сложность кода втрое
Мои глубочайшие соболезнования.
если мантру повторять, то можно опровергнуть и действительность
Моя "мантра" хотя бы основана на опыте использования и общей удовлетворенности комьюнити языком. Твоя мантра возникла на пустом месте и не имеет оснований, кроме личных заблуждений.
Какая разница? Это одно и тоже. Вместо анонимного блока - объявляешь анонимную функцию с понятной областью видимости.
А так блок в блок бы вкладывал. Разница чисто синтаксическая, механизм был бы тем же, скорее всего.
Мне не нравятся Го-шные defer не из-за синтаксиса, а из-за привязки к времени жизни функции. Привязка к блоку, как в большинстве языков (RAII, или finally, или defer, где он тоже есть) выглядит намного органичнее.
Я понимаю, почему было привязано к функции: 1. так проще реализовать (просто список колбэков без сложностей со стороны компилятора), 2. изредка удобнее (если инициализация ресурса и, соответственно, defer на его «отпускание» зависят от условия).
Но всё же привязку к блоку хочется чаще.
Так как Go — это open-source, то возможно:
Сделать форк и назвать его MyIdealGo.
Запилить все фичи, которые, кажется, облегчат жизнь.
Распространить примеры (что, как, зачем).
Создать PR и убедить мантейнеров языка в нужности фич.
Profit.
Пожелаю Вам заранее успеха)
Вы не правы во всём. В каждом описанном вами случае это была не лень, а выбор между различными компромиссами. И то, что выбор разработчиков Го не совпал с вашими предпочтениями, не говорит о их лени. Просто у них были другие предпочтения.
А если Вы заговорили о лени, то где же написанный Вами язык, реализующий все Ваши предпочтения? Лень писать?
если она выдаст задокументированную панику, то всё ok.
И вместо if err != nil на каждый вызов будем писать if r := recover(); r != nil
Ну спасибо, ну удружил! (на самом деле нет)
Странное понимание о читаемости кода.
comments, err := getComments(posts[0].ID)
if err != nil {
return err
}
comments := utils.Must(getComments(1))
В первом варианте видно явные намерения, как во втором неявное.
И думаю не надо использовать panic вместо throw, его поведение отличается.
Надеюсь, кто такое предлагает, не использует на реальных проектах, в которых участвуют много людей.
Если не нравится реализация данной концепции в go, возьмите подходящий инструмент. Как вариант https://harelang.org/tutorials/introduction#handling-errors
Думаю, ну для этого необходимо нормально разбить простыни на небольшие функции или методы с понятными неймингами. А во втором варианте будет постоянно мозг вспоминать, а что же там такое происходит магическое, и 100 человек по-разному будут делать выводы и тормозить, перепроверяя себя. В отличие от языков, где такое на уровне синтаксических конструкций и предусматривает определенное поведение.
Сугубо личное мнение, что мало букв не равно лучшее понимание.
первый вариант втрое (втрое!) длиннее
И что? Clear is better than clever.
при увеличении кодовой базы намерения программиста перестают быть видны
Нет.
После utils.Must(..... кусок важный забыли!
if r := recover(); r != nil { // TODO handle error here } - вот этот)
Проблема такого подхода в том, что вы не решаете единственную причину, по которой error тип вообще существует в go.
Нужно всегда задать себе вопрос, что случится, если моя функция будет исполнена в короутине? Го не содержит стандартного супервизора, как эрланг, акка etc. Нет и аналога в виде структурной многозадачности и ланчеров выполняющих эту роль в kotlin.
Сигнатура функции чётко говорит о том, что функция может отработа нештатно вернув error тип.
Сама по себе panic'a может быть вообще чем угодно - интерфейсом error, строкой, числом, набором байт - чем угодно. Это слишком низкоуровневый примтив, чтобы им можно было бы пользоваться как "исключением".
Эту проблему можно решить через recover паники в дефёре и превращение его в ошибку для публичной функции/метода. ваша Must функция может вообще именоваться как Try, а дефёр функция как Catch.
func Something(ctx context.Context, arg1 int, arg2 string) (empty Data, err error)
defer dry.Catch(&err)
var res Data
res.Call1 = dry.Try(Func1(ctx, arg1))
res.Call2 = dry.Try(Func2(ctx, arg2))
return res, nil
}
Т.к. go это просто довольно низкоуровневая имплементация сопрограмм и их рантайма, вы вполне можете подойти таким образом. Но столкнётесь с кучей вопросов.
Как оборачивать исключения выброщенные непосредственно вами, как оборачивать паники полученные не от вас, что делать с стек-трейсом (для паники, которая не является error типом, неплохо бы передавать наверх стейт оригинальной паники).
Просто написать panic - никогда не бывает достаточно.
Это тот же самый catch'er положенный в defer. Который просто потребует больше кода, от пишущего код.
А это вы определили, что не требуется или какое-то практическое исследование было?
Вполне себе нормальная история, когда получив (конкретную) ошибку, вы маршрутизируете код в другом направлении. В случае если код будет выкидывать panic - написание и поддержка такого кода усложнится на 2 порядка.
Относительно оператора в rust, я бы не сказал, что это прекрасное решение. Более того, я скажу, что оно отвратительное - почему у меня каждая строка кода вызывающего interop встала под вопрос? После шахмат и kotlin у меня предубеждение к такому подходу. Знаки препинания скорее показывают проблемы в коде, чем решают их.
Я бы сказал, что эта проблема должна решаться на уровне компилятора. Если в сигнатуре на стеке есть error возврат - то самим компилировать блок возврата для каждой строки где не написано "_" для ошибки.
Давайте начнём с основ. Для чего используют Го? Для написания сервисов. Сервис - это программа, которая постоянно запущена и обсуживает множество запросов. Пришёл запрос, отдаём ответ. Есть ли в работе сервиса место панике?
Но сервисы действительно бывают большими и сложными. Всегда можно выделить базовые функции: получить пользователя, получить связанные данные. Может случится, что нет такого пользователя. Или нет подключения к базе данных. Только вот в рамках запроса надо ведь не в панику падать, а давать конкретный ответ. И принимать следующий запрос и давать на него ответ. А в фоне можно и переподключится к БД.
В рамках сервиса паника - исключительная ситуация. Когда понятно, что дальше можно испортить данные работой сервиса, например. И при масштабировании проекта как раз это легко уловить. Если же у вас возникает длинная цепочка вызовов, которая только пробрасывает ошибку с самого низа, то тут вопрос к вашей архитектуре.
Не традиционный подход
А какой?
Го'шники переизобретают монады... Исключения через панику и дефер уже переизобрели...
Думаю, не я один считаю Го второй пыхой. Там тоже много чего "нового" и "инновационного" изобрели 😁 И тоже вначале хвастались, что фреймворки не нужны, а потом как "понеслось", как при сливе унитаза кое что по трубам... Пока не родили симфони, который как две капли воды похож на явовский гибернейт (с элементами спринга оттуда же) Только явисты к тому времени уже более 10 лет этим всем пользовались... Думается мне, с Го будет та же песня. Ребята, хорошая вещь "с кондачка" не делается. Нельзя просто сказать "а у нас будет не так, как у других! Другие раздувают код, а мы такие вумные, все сделаем по простому, выкинув все лишнее". Это не работает. История ИТ знает наверное сотни, если не тысячи подобных итераций с разными технологиями и платформами. Причем, почему то такая фигня только в ит. Слишком, видимо, просто выкинуть старое и сделать свой велосипед с нуля, "не такой как у всех"
Не надо вот этот ужас записывать под гошников, автор явно языком либо много не пользуется, либо изучил недавно, ни один гошник, понимающий суть языка, не станет изобретать исключения, тем более через паники.
Плюсую. Го - это и вторая пыха, и вторая жаба.
Я работал в кодовой базе на Го, написанной бывшими джавистами. И сперва не понимал, зачем они делают так сложно. Со временем стал осознавать, что это не «сложно», а на самом деле «необходимо». (Ок, я чуть привираю: до конца я так и не осознал, но стал понимать мотивы и некоторые сперва неочевидные плюсы).
Простые вещи действительно хотелось бы делать простыми вещами и способами. Но когда проект растёт, простые способы перестают работать.
Энтерпрайзные фреймворки изначально сделаны для сложных вещей, и пока ты до таких вещей не дорос, фреймворки кажутся избыточными. Но для сложных вещей фреймворки сложность превращают в рутину: есть стандартные решения стандартных задач, и 95% твоих задач - стандартны.
Дисклеймер: я люблю Го в его простоте.
В итоге у вас будет огромный defer с перехватом паники, где будет отдельно определяться тип и приниматься решения. Самая жопа начнется при отладке, особенно когда у вас несколько гороутин.
среднестатистический Go-код не определяет тип ошибки.
Статусы grpc передаются через ошибки.
И вот вам примеры из прямо сейчас открытого мной микросервиса.
errors.Is(err, context.Canceled) - 11 раз
errors.Is(err, io.EOF) - 10 раз
var he *echo.HTTPError if errors.As(err, &he) { - 1 раз
Есть еще бизнесовые, их не буду прикладывать из-за NDA.
Это только один сервис, у нас их больше 20.
Обработка ошибок в Go — Не традиционный подход