Фактически, это продолжение спора, завязавшегося в комментариях к заметке “Почему я не использую языки 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-а) не кажется мне надежным.