Фактически, это продолжение спора, завязавшегося в комментариях к заметке “Почему я не использую языки D и Go”. Прочитал о таких механизмах языка Go, как defer, panic и recover (ссылка #1 на блог разработчиков языка Go, ссылка #2 на Effective Go). Поскольку на Go я не программировал, то буду рассказывать то, что я понял. А это может быть совсем не то, что есть в действительности ;)
Итак, что же такое defer, panic и recover в Go и зачем они там? Ключевое слово defer позволяет зарегистрировать вызов функции, который должен быть сделан автоматически при выходе из текущей функции. Например, пусть мы открываем файл и должны гарантировать его закрытие при выходе из функции. Механизм defer позволяет нам это сделать просто и элегантно:
file, err := os.Open(filename, os.O_RDONLY, 0) if err != nil { // Какая-то ошибка... // Обрабатываем ее и уходим. return err } // Ошибок нет, файл должен быть закрыт. defer file.Close() // Теперь файл автоматически будет закрыт. |
Если в текущей функции сделано несколько обращений к defer, то зарегистрированные отложенные вызовы будут произведены в обратном порядке (по аналогии с тем, как в C++ происходят вызовы деструкторов объектов). Собственно и назначение defer аналогично деструкторам C++ – очистка ресурсов.
Но, в отличии от деструкторов C++, отложенные функции в Go могут изменять возвращаемое значение той функции, в которой был зарегистрирован их отложенный вызов. Например:
func sample() (res int) { defer func() { res = 1 }() return 0 } |
Если вызывать sample(), то вернет она 1, а не 0, т.к. возвращаемое значение замещается в отложенной функции. Сделано это для того, чтобы можно было управлять возвращаемым значением в случае panic-ов.
Конструкция panic прерывает выполнение текущей функции и начинает “раскрутку стека”. В этом процессе вызываются лишь все зарегистрированные через defer функции (т.е. программисту дается возможность очистить ресурсы). Если процесс раскрутки стека доходит до корня текущей goroutine, то приложение аварийно завершается.
Но, в случаях, когда приложение может восстановиться после сбоя, возникший panic можно перехватить с помощью инструкции recover. Вызывать ее имеет смысл лишь в отложенных функциях. Если recover возвращает nil, то раскрутка стека сейчас не выполняется. Но если recover возвратила отличное от nil значение, значит сейчас идет раскрутка стека в результате panic. И возвратила recover как раз объект, переданный в panic. Например:
func openAndReadFileContent() (content string, err os.Error) { // Регистрируем отложенную функцию, которая все panic-и // будет преобразовывать в отрицательный ответ (если паника // была вызвана os.Error). defer func() { if r := recover(); r != nil { err = r.(os.Error) // Это, на самом-то деле, хитрая строка. } } file, err := os.Open(someFileName, os.O_RDONLY, 0) if err != nil { return nil, err } defer file.Close() content := readAndParseFileContent(file) return content, nil } func readAndParseFileContent(file File) string { // Выполнение чтение исодержимого файла и при каждой // ошибке порождение паники. rawContent, err := ioutil.ReadAll(file) if err != nil { panic(err) } ... // Преобразование прочитанного содержимого. } |
Здесь в начале функции openAndReadFileContent регистрируется отложенная функция, которая преобразует панику в обычное возвращаемое значение. Что позволяет писать саму функцию openAndReadFileContent (и все подчиненные ей функции) так, чтобы инициировать панику в любых ситуациях, когда что-то идет не так.
После такого короткого введения выскажу свои соображения. Фактически это те же самые исключения. Только Go-шные, а не привычные большинству мейнстримовых программистов. Можно было бы сказать, что в сущности это Фаберже, автопортрет, вид в профиль, фрагмент. Однако…
Лично мне кажется, что обычные исключения все-таки лучше.
Во-первых, в Go исповедуется стиль с возвращением ошибок. Т.е. код будет пестреть if-ами с последующими return-ами. Такой стиль увеличивает объем кода и усложняет его восприятие. Но самое важное, он череват опасностью забыть написать if и, тем самым, проглотить ошибку.
Отсюда вытекает и во-вторых. А именно: вот когда разработчику использовать коды ошибок, а когда panic? Если язык программирования изначально поддерживает исключения (скажем, как Ruby), то там все просто – все библиотеки кидают исключения и ты сам пишешь в том же духе. А что здесь?
В-третьих, если все-таки panic-и начинают использоваться как исключения (например, в блоге разработчиков Go советуют посмотреть, как этот прием используется в модуле разбора JSON-а), то получается, что разработчик пишет свой аналог catch (через отложенную функцию с обращением к recover). Но если в языках с исключениями (вроде C++, Java, C#, Python, Ruby и в том же Erlang-е) это намерение разработчика явно декларируется специальной языковой конструкцией try-catсh вокруг проблемного кода, то в Go все это дело уводится в отложенный вызов (который далеко не всегда будет лямбда-функцией). Что не есть хорошо.
В-четверных, в языках с исключениями принято делать иерархии исключений. А конструкции catch позволяют ловить исключения как по конкретным классам, так и по целым семействам исключений. Причем делается это, опять же, посредством специального синтаксиса. Скажем, если я использую библиотеку для шифрования, у которой корнем иерархии исключений является класс CryptoError, то я могу легко записать перехват только этих исключений и игнорирование всех остальных. А вот в Go подобные иерархии, в принципе, можно выстраивать, но потом определение типа ошибки нужно будет писать вручную:
func (d *decodeState) unmarshal(v interface{}) (err os.Error) { defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { panic(r) } err = r.(os.Error) } }() |
Здесь ошибку поймали, проверили, принадлежит ли оно семейству runtime.Error. Если принадлежит, то пробросили эту ошибку дальше (посредством еще одного вызова panic). А если это не runtime.Error, то считается, что это os.Error и именно os.Error возвращается.
Как по мне, так запись (это язык Ruby):
begin doSomething rescue OsError => x return x end |
представляется более простой, компактной и надежной.
Так что я бы предпочел иметь обычные исключения, а не Go-шные panic и recover-у. Ну, а аналог defer-а есть в том же D в виде конструкций scope(exit). Да еще и более гибкий, имхо.
PS. Кстати, на счет очень хитрой сроки err=r.(os.Error). Хитрость ее заключается в том, что это приведение типа. И если r не принадлежит типу os.Error то будет сгенерирована новая паника (это если я правильно понял документацию). Так что в связи с этим последний пример с функцией unmarshal (взятый как раз из модуля декодирования JSON-а) не кажется мне надежным.
Эрланг ты зря в языки с исключениями записал :)
ОтветитьУдалитьПо факту они там есть, но намного идеоматичней возврат статус (аля ok | {error, ErrorDetails} ), а ПМ делает ненужными тупые ифы как в сях.
@Qrilka:
ОтветитьУдалитья его туда записал, поскольку:
- он уже упоминался в том флейме. И, с моей точки зрения, добавление исключений в Erlang как раз является признанием факта их полезности;
- насколько я понял по фрагментам исходников OTP, конструкции trу-catch в Erlange являются специальной разновидностью ПМ и намного больше похожи на try-catch в C++/Java/C#, чем обработка recovery в Go.
А идеоматичные Эрлангу коды возврата ты просто "скипнул"? И настойчиво обсуждаешь нечасто используемый try catch? (Введённый, кстати, не с самого начала в язык)
ОтветитьУдалитьИ интересно, что за "исходники OTP" ты смотрел.
Можно ссылкой на файлик с гитхаба - http://github.com/erlang/otp/
>А идеоматичные Эрлангу коды возврата ты просто "скипнул"?
ОтветитьУдалитьНе вижу смысла в теме об исключениях в Go и D говорить об кодах возврата в Эрланге.
По факту исключения в Эрланге есть? Или это миф?
В С++ исключений так же не было. И даже сейчас в C++ исключения используются не часто. Тем не менее, тенденция к увеличению их использования явная. Не исключено, что и в Эрланге процесс идет в том же направлении.
>Можно ссылкой на файлик с гитхаба - http://github.com/erlang/otp/
Да как два байта:
http://github.com/erlang/otp/blob/dev/lib/compiler/src/compile.erl -- строка 986, например. Практически чистой воды try-catch из C++/Java подобного языка.
http://github.com/erlang/otp/blob/dev/lib/asn1/src/asn1ct_gen.erl -- сколько здесь будет catch-ей.
Еще можно заглянуть в разные cos*/src/
А можно просто grep натравить и найти несколько сотен файлов, в которых try и/или catch задействован.
Из первого же файла пример варианта "не по исключениям":
ОтветитьУдалитьok = file:close(Lf)
Я просто хотел обратить твоём внимание на то, что твои словам про "код будет пестреть if-ами" справедливы не для всех языков.
>Я просто хотел обратить твоём внимание на то, что твои словам про "код будет пестреть if-ами" справедливы не для всех языков.
ОтветитьУдалитьПолностью с этим согласен. Данное утверждение я относил только к Go. Если же из текста заметки выходило, что данная фраза относилась к нескольким языкам, то это моя недоработка.
Erlang я упоминал вот почему. За счет конструкции catch в Erlang очень легко работать и с кодом, который порождает исключения, и с кодом, который использует традиционную схему с кодами возврата. Т.е. если изначально разработчик написал что-то вроде:
case some:func(...) ...
а потом выяснилось, что нужно бы еще и обработать выскакивающие из some:func исключения, то достаточно написать:
case catch some:func(...) ...
и всех делов.
Это дает возможность более просто применять исключения в Erlang, чем panic-и в Go.
> В С++ исключений так же не было. И даже сейчас в C++ исключения используются не часто. Тем не менее, тенденция к увеличению их использования явная. Не исключено, что и в Эрланге процесс идет в том же направлении.
ОтветитьУдалитьИсключено.
Да, никаких существенных возражений по поводу схемы обработки ошибок в Go в твоей статье не содержатся. Набор впечатлений.
ОтветитьУдалитьТак что и спорить не о чем.
@Gaperton:
ОтветитьУдалить>Да, никаких существенных возражений по поводу схемы обработки ошибок в Go в твоей статье не содержатся. Набор впечатлений.
А заголовок поста прочитать не судьба? Речь изначально шла о впечатлениях.
>Так что и спорить не о чем.
А смысл какой? Ты можешь повлиять на развитие Go?
Я не могу, да мне это и не нужно. Так что и смысла спорить нет.
Я, к сожалению, вообще не замечаю у Go какого-либо развития.
ОтветитьУдалитьТак что влиять не на что :).
Но язык крайне интересен с теоретической точки зрения своим отношением выразительности с простоте. Такое редко встретишь.
Вот, скажем, казалось бы, эксцепшнов в нем нет. Но - тот же эффект легко достигается без и введения отдельной концепции.
_Достигается_, а то, что делается это не так, как ты к этому привык - вопрос эстетики. Можно успешно аргументировать и за, и против такого способа, но косяка в _отсутствии_ исключений нет.
То же самое касается классов. Их, как отдельного понятия, нет. Но...
И так далее.
>_Достигается_, а то, что делается это не так, как ты к этому привык - вопрос эстетики. Можно успешно аргументировать и за, и против такого способа, но косяка в _отсутствии_ исключений нет.
ОтветитьУдалитьПолностью согласен. Но как раз из-за несоответствия моим эстетическим пристрастиям язык Go не интересен мне как потенциальный практический инструмент.
Т.е. разбирая каждую его часть в отдельности можно понять зачем она, почему она именно такая и как ее использовать. Но... не торкает, ни по отдельности, ни в целом.