В последнее (и не только последнее) время ломают много копий по поводу неудобства обработки ошибок в Go.
Суть претензий сводится к тому, что прямое использование:
customVar, err := call()
if err != nil {
doSomething(err)
return err
}
на больших количествах повторений гораздо менее удобно, чем классическое:
try {
String customVar = call();
} catch (BadException e) {
doSomething(e);
sendException();
}
Можно долго спорить как по самому предмету претензий, так и по поводу обходных манёвров, однако же логика в «пакетном» подходе действительно имеется.
В связи с чем у меня и возникла мысль по поводу обработки исключений без особого отхода от «Go-way». Вариант не рабочий — всего лишь моя фантазия.
Выглядело бы это так:
try err {
customVar1, err := call1()
customVar2, err := call2()
customVar3, err := call3()
} catch {
doSomething(err)
return err
}
Общая идея такова: сразу после try объявляется переменная (в нашем случае err) типа error, область видимости которой — весь блок try...catch. Далее, при каждом присвоении переменной нового значения внутри блока try, компилятор проверяет его на nil, и если вернулась ошибка — следует переход в блок catch. Теоретически это не слишком затратная операция, производительность не должна пострадать.
Также возможно назначение нескольких переменных, типа:
try errIo, errNet {
customVarIo1, errIo := callIo1()
customVarIo2, errIo := callIo2()
customVarNet1, errNet := callNet1()
customVarNet2, errNet := callNet2()
} catch {
if errIo != nil {
doSomething(errIo)
return errIo
} else {
doSomething(errNet)
return errNet
}
}
В данном случае пример не слишком наглядный, однако же при большом количестве кода внутри try бывает полезно сгруппировать ошибки, чтобы в catch их обработка не сводилась к:
switch err {
case net.Error1:
doSomethingWithNetError()
case net.Error2:
doSomethingWithNetError()
case io.Error1:
doSomethingWithIoError()
case io.Error2:
doSomethingWithIoError()
}
В общем, у меня всё. Ругайте.
Комментарии (44)
Blooderst
03.11.2015 13:31+3При необходимости на Go легко реализуется поведение, подобное try/catch, при чем без лишнего оверхеда по сравнению с теми языками, где это выделено в отдельную сущность. Но, по причине отсутствия явной поддержки исключений в языке, в Go это реализуется в более многословном виде без синтаксического сиропчика. Но в случае уместности такого подхода это вполне идиоматично и именно это и нужно делать и именно это и делается, в том числе в стандартной библиотеке языка.
Но как показывает многолетняя практика разных языков программирования, наличие инструкций try/catch провоцирует использование этого инструмента везде и всегда, тогда как на самом деле такой подход к обработке ошибок оправдан лишь в небольшом количестве случаев. Именно поэтому эта функциональность выпилена из языка с корнем. Это компромисс, который заставляет иногда реализовывать try/catch в ручную ради искоренения тотального засилья трай-блоков во всем коде.
Это сделано умышленно и это ни когда не поменяется. И эта особенность не преподносится как идеальное решение, это не преподносится как эталон для подражания, это не преподносится как ноу-хау. Это сделано из соображений циничного прагматизма. И как у любого компромисса, эта особенность языка имеет и слабые и сильные стороны, но в целом этот подход приводит к более качественному и более поддерживаемому коду.
s_kozlov
03.11.2015 15:48Очень интересно было бы посмотреть на реализацию try/catch в Go руками. Пишу без сарказма, действительно интересно.
Мне на ум приходит только panic/defer/recovery, но этот подход требует создания оберток функций для блоков try/catch, что сильно усложняет код и портит его обозримость. Не говоря уж о том, что это неправильный подход для Go.divan0
03.11.2015 17:08OlegFX
03.11.2015 22:54+1Главный вопрос в ловле panic — производительность. Мне кажется, она тут страдает.
bolk
04.11.2015 04:43Почему она не страдает в случае try…catch, но страдает в случае panic…defer?
isden
04.11.2015 11:18+1> Почему она не страдает в случае try…catch
Очень даже страдает. В той же Java, например.OlegFX
04.11.2015 12:10+1Немного потеоритезирую, но в случае имплементации моей версии try...catch компилятор после каждого присвоения нового значения указанным после try переменным типа error — сразу после того, как записал их значение (коим является 64-битный адрес объекта ошибки) в память, но до того, как вытеснил это значение из регистра — просто смотрит, является ли значение в регистре отличным от нуля. Если да — следует простой переход в блок catch, а все нужные значения при этом уже находятся на своих местах. Никаких других накладных расходов, вроде, не предвидится. Не думаю, что в данном варианте логики производительность сильно пострадает. Всего лишь одна проверка регистра на 0.
Полноценное исключение, насколько я его понимаю, является гораздо более масштабным мероприятием.neolink
04.11.2015 12:16> try err {
> go func() {
> err = MyCoolFunc()
> } ()
> } catch {
>
> }
как будет работать такой код?OlegFX
04.11.2015 12:20Хороший вопрос.
Я бы не ловил в данном случае. То есть catch ловит только внутри текущей goroutine.neolink
04.11.2015 13:57+1ну то есть мало проверить на nil надо ещё тогда проверить кто его туда записал, значит надо записать куда-то кто его туда записал и так далее
OlegFX
04.11.2015 21:34Не надо ничего проверять. Это делает компилятор — для кода, относящегося к текущей goroutine, расставляет в местах инструкций для процессора переход по условию на точку входа в блок catch. Чуть выше я написал как именно он это делает.
vGrabko99
03.11.2015 18:21Блоки try/cath выглядят весьма уродливо. Они запутывают структуру кода и смешивают обработку ошибок с нормальной обработкой. Что мешает сделать как то так?
if err := client.Set("Fatal", " ", 0).Err(); err != nil { log.Print(err) }
neolink
03.11.2015 18:39с одной стороны да, но пример из статьи так не записать без явного обявления переменных customVar и err
justmara
03.11.2015 21:26-1т.е. вот это менее уродливо?
client.Set(«Fatal», " ", 0).Err()
vGrabko99
03.11.2015 21:29-1это redis либа если что. Где
client — индикаторный соединения
Fatal — имя ключа
" " — содержание.
Ну а если не нравиться можно написать свою либу. Благодаря Go я это могу сделать за 30 минут просто изучив команды редиса.
neolink
03.11.2015 21:49менее уродливо чем что?
justmara
03.11.2015 22:31Чем try/catch, в качестве альтернативы которому был приведён этот код.
Т.е. вызов метода Err() у результата void-метода Set(), по мнению, vGrabko99, меньше "запутывает структуру кода" и не "смешивает обработку ошибок с нормальной обработкой".vGrabko99
03.11.2015 22:48согласен! Пример не удачен. Вот обработка ошибок бд (не кривая либа с гитхаба, а коробочная)
if rows, err := DB.Query("SELECT * FROM users WHERE login=?", login);err !=nil { log.Fatal(err) }
Вы тупо вместо трех этажных конструкций всё делаете сразу в if.OlegFX
03.11.2015 22:57В данном конкретном случае областью видимости rows будет блок if/else. Не всегда это удобно.
vGrabko99
03.11.2015 23:14+1Все равно удобнее try/cath (я видел очень много этих try/cath и хочу сказать что даже передача rows в глобальную переменную для функции в else блоке будет намного более читаемо и понятно)
neolink
03.11.2015 23:16+1ну это не корректно с try/catch тут только if,err надо сравнивать, остальное дизайн библиотеки (конкретно эта возвращает чуть больше чем err)
то есть
> if err := client.Set(«Fatal», " ", 0); err != nil {
> log.Print(err)
> }
vs
> try {
> client.Set(«Fatal», " ", 0)
> } catch (Exception e){
> log.Println(e.getMessage())
> }
в этом случае вариант с if явно короче.
по поводу ошибки как значения против исключений можно холиварить много, но не надо тащить в Go практики работы с исключениями.qw1
04.11.2015 11:04+3Вся суть обработки исключений в том, что не надо проверять результат после каждого библиотечного вызова.
По моему опыту, большинство функций вообще обходятся без try. Обработка ошибок не видна и не засоряет код.
В try заворачивается весь main или вся транзакция, или обработка взаимодействия с одним подключением (отвалилось — залогировали и забыли). В тех случаях, когда ошибки ожидаются, нужно вызывать фунции со статусом-результатом (TryParse вместо Parse, TryGetValue вместо обращения к Dictionary по индексу). Да, методов в библиотеках становится больше, зато писать код быстрее.
Go не даёт выбора.neolink
04.11.2015 11:47-1> По моему опыту, большинство функций вообще обходятся без try. Обработка ошибок не видна и не засоряет код.
Вот это именно то к чему склоняют исключения — генерировать ошибки, но не обрабатывать ошибки, а просто поймать их где-то в общем для всего приложения месте.
> Go не даёт выбора.
а ещё выбора не дает Rustqw1
04.11.2015 13:32Вот это именно то к чему склоняют исключения
Они склоняют, а мы не поддаёмся. Исключения не должны быть единственным способом передачи ошибки, как я писал выше. Программист должен взвешенно выбрать, что использовать.
Конструкции типаtry { client.Set(«Fatal», " ", 0); } catch (Exception e)
это антипаттерн, не надо их приводить в пример.neolink
04.11.2015 13:47Я не его приводил в пример, а просто написал эквивалентный вариант по Go коду.
В Rust и Go на уровне дизайна разделены случаи ошибок и исключительных ситуаций, зачем пытаться делать ещё одну сущность и собрать их вместе?
Как мне писать в этом случае код работайщий со сторонним кодом, когда у меня одна функция возвращает error, а вторая исключения, stack trace будет формироваться в обоих случаях? почему в с# это сделано только для некоторых классов но не для всех (уж не потому, что иметь 2х методов это пзцд)?
Почему в статьях про Rust — http://habrahabr.ru/post/242269/ большинство говорят, что да это классно, а в статьях про Go — как там могли придумать вообще, верните мои исключенияqw1
04.11.2015 17:11Я не его приводил в пример, а просто написал эквивалентный вариант по Go коду.
В принципе плохой пример. Ошибка логируется, а программа продолжает как ни в чём не бывало и падает на следующей строке из-за того, что предыдущий оператор не выполнился.
Хороший пример — это вернуть err в вызывающую ф-цию.
Тогда аналог на c#/java вообще бы не содержал бы никаких try/catch, ни одного лишнего символа в коде на обработку ошибок. Исключение прозрачно ушло бы наверх.
Как мне писать в этом случае код работайщий со сторонним кодом, когда у меня одна функция возвращает error, а вторая исключения, stack trace будет формироваться в обоих случаях?
На нагрузку на стек никак не влияет наличие try/catch, ведь trace надо будет собрать в любом случае, если где-то в глубине дерева вызовов случится panic.
FanKiLL
04.11.2015 17:47Что то мне это напоминает, ах да это же java а именно try-with-resources
try (Statement stmt = con.createStatement()) { ResultSet rs = stmt.executeQuery(query); while (rs.next()) { System.out.println(---); } } catch (SQLException e) { JDBCTutorialUtilities.printSQLException(e); }
Ваш подход очень похож. И не так уж плох.
guai
07.11.2015 15:25+1было бы прикольно на уровне языка иметь возможность вызывать метод так или этак, с возвратом ошибки или с выбросом исключения.
Например: result, err = foo() и result,! = foo()
второй вариант это типа получить ошибку и сразу выбросить ее как исключение.
ну или так: foo() и foo!()qw1
07.11.2015 16:14Этим можно пользоваться, если все нижележащие функции возвращают ошибку.
Как только мы написали foo!, наша функция начинает кидать исключение и к ней этот подход перестаёт работать.
Конечно, можно ловить исключение и преобразовывать его в ошибку, но программист никогда не будет уверен, то с чем он сейчас работает, не кидает ли скрытые исключения, просаживая производительность.guai
07.11.2015 16:37компилятор имхо сможет не собирать стэктрэйсы на тех путях исполнения кода, где они в итоге не будут нужны. Хотя сколько на само это ресурсов потратится — хз. Ну и на сам язык ограничения могут возникнуть как следствие…
s_kozlov
как мне кажется, уж лучше использовать наработанные стереотипы от других языков. Вместо
лучше использовать форму
Core2Duo
Ага, и вместо
использовать обычный подход:
И, внезапно, получаем стандартную инструкцию try/catch. Потому что она уже продумана и испытана во многих языках. Но, по какой-то причине, это не go-way.
psylosss
Не по какой-то, а по вполне определённой — создатели языка верят, что использование исключений приводит к запутанному коду.
OlegFX
Если ловить исключения, и делать это так, чтобы было максимально похоже на другие языки — возможно, но смысл конструкции с объявлением переменных сразу после try в том, что там происходит их инициализация. В противном случае мы в catch можем искать переменные, которые не инициализированы — до них просто очередь не дошла.
Ну это, конечно, если не ломать сам подход к обработке ошибок. Если ломать — придётся устроить большой холивар на тему «в какую сторону» и «нужно ли». И мне почему-то кажется, что за «ломать» будут в первую очередь те, кто на Go не пишут, а большинство среди тех, кто будет «против» — использует этот язык ежедневно.