Пишу, по большей части, про историю, свою жизнь и немного про программирование.

«Go» паникует

Попробую рассказать ещё об одной вещи, которая показалась мне в «Go» странной, дополню предыдущий пост.

Иногда, скомпилированная программа, написанная на «Go» «паникует»:

bolk-osx:Sample bolk$ 6g t.go && 6l t.6 && ./6.out
panic: runtime error: index out of range

runtime.panic+0xac /Users/bolk/go/src/pkg/runtime/proc.c:1083
    runtime.panic(0x11ea8, 0xf8400011f0)
runtime.panicstring+0xa3 /Users/bolk/go/src/pkg/runtime/runtime.c:116
    runtime.panicstring(0x295e6, 0xce63)
runtime.panicindex+0x25 /Users/bolk/go/src/pkg/runtime/runtime.c:73
    runtime.panicindex()
main.main+0x5e /Users/bolk/Проекты/Sample/t.go:6
    main.main()
runtime.mainstart+0xf /Users/bolk/go/src/pkg/runtime/amd64/asm.s:77
    runtime.mainstart()
runtime.goexit /Users/bolk/go/src/pkg/runtime/proc.c:150
    runtime.goexit()
----- goroutine created by -----
_rt0_amd64+0x8e /Users/bolk/go/src/pkg/runtime/amd64/asm.s:64

То есть возникает ошибка, которая аварийно завершает выполнение программы. Самый простой способ этого добиться — вызов «panic», более реальный — обратиться за границу массива:

package main

func main() {
    arr := []int{1,2,3}

    print(arr[1000])
}

«Паника», конечно, ситуация исключительная, программист должен постараться не допускать ситуации, в которых она возникает, но всё же, программа «паникует», а нам нужно продолжить её выполнение, например, чтобы произвести какие-то завершающие действия. Что делать?

Я спросил пару человек, незнакомых с «Go», что, по их мнению, язык должен бы предоставить программисту для решения этой проблемы, оба человека назвали обработку исключений. К сожалению, тут исключений нет. Подход к решению этой проблемы у «Go» чем-то напоминает Visual Basic, а именно конструкцию On Error GoTo line.

В «Go» есть поистине замечательная штука — defer, с её помощью можно вызвать функцию когда управление покидает определённую зону видимости. Для языка без деструкторов (а в «Go» их нет) это очень удобно — можно закрывать файлы, сокеты и прочее в таком полуавтоматическом режиме — важно не забыть поставить нужный вызов только один раз.

Функции с defer будут вызваны когда управление покидает выбранную нами зону по любой причине, в том числе, когда произошла «паника». В функции можно проверить, была ли она вызвана нормальным ходом событий или что-то пошло не так, вызов этой проверочной инструкции (он называется «recover») «успокаивает панику».

Выглядит это вот так:

package main

func main() {
    defer func() {
        if r := recover(); r != nil {
            print("Recovered")
        }
    }()

    arr := []int{1,2,3}

    print(arr[1000])
}

Происходит тут следующее: в зоне вызова функции «main» (тут, как в Си, при запуске вызывается «main») определена анонимная функция, которая тут же вызвана как defer, при возникновении ошибки (обращение за границу массива в предпоследней строке), управление покидает зону видимости функции «main» из-за чего вызывается наша анонимная функция, которая проверяет из-за чего она была вызвана (присваивает значение, полученное из recover, переменной „r“ и проверяет его на nil), чем «успокаивает панику», если она была, и выводит сообщение «Recovered» в этом случае.

6 комментариев
zg (zg.livejournal.com) 2011

что ж это за паники такие, после которых можно восстановится.

Евгений Степанищев (bolknote.ru) 2011

Комментарий для zg.livejournal.com:

Я привёл пример — выход за границу массива. Если хочется ещё пример, то вот этот код «запаникует» divizion by zero:

package main

import «big»

func main() {
    a := big.NewInt(0)
    b := big.NewInt(0)

    print(a.Quo(a, b))
}

Метод Quo описан в документации: http://golang.org/pkg/big/#Int.Quo

zg (zg.livejournal.com) 2011

Комментарий для Евгения Степанищева:

ну так вышли, падать надо, а не восстанавливаться.

Евгений Степанищев (bolknote.ru) 2011

Комментарий для zg.livejournal.com:

Не понял мысли. Зачем тут падать? Ну вышли, делов-то. Компилятор отловил ситуацию и как-то сообщил об этом программисту, падать-то зачем? Вот Пайтон же не падает:

bolk-osx:Sample bolk$ python -c «try: a=(); print a[2]
except: print(’Recover’)»
Recover
bolk-osx:Sample bolk$

zg (zg.livejournal.com) 2011

Комментарий для Евгения Степанищева:

ну в питоне — это ж исключение, не?
есть места, где есть и исключения, и паники. исключения ловятся и используются в ситуациях: не хватило памяти, не смогли открыть файл, не удалось соединиться с удалённым хостом и т. д. паники приводят сразу к закрытию приложения. вызываться они могут в том числе и при выходе за границы массивы, при копировании строк, когда в строке назначении места не хватает, при доставании со стека больше, чем положили и т. д.
ведь обращение за пределы массива однозначно свидетельствует о логической ошибке в программе. зачем ей вообще работать с логической-то ошибкой?

Евгений Степанищев (bolknote.ru) 2011

Комментарий для zg.livejournal.com:

ну в питоне — это ж исключение, не?

Исключение. Исключение в Пайтоне это не что-то исключительное, там оно сплошь и рядом, рекомендуется использовать их как можно более широко. Например, у строки есть метод, который порождает исключение, если подстрока не найдена.

исключения ловятся и используются в ситуациях: не хватило памяти, не смогли открыть файл, не удалось соединиться с удалённым хостом

Вот я как бы и намекаю на то, что из-за того, что в языке нет исключений, используется паника. Взглянем, например, на внутренности модуля regexp ( http://golang.org/src/pkg/regexp/regexp.go#L618 ):

   618 func Compile(str string) (regexp *Regexp, error os.Error) {
   619 regexp = new(Regexp)
   620 // doParse will panic if there is a parse error.
   621 defer func() {
   622 if e := recover(); e != nil {
   623 regexp = nil
   624 error = e.(Error) // Will re-panic if error was not an Error, e. g. nil-pointer exception
   625 }
   626 }()
   627 regexp.expr = str
   628 regexp.inst = make([]*instr, 0, 10)
   629 regexp.doParse()
   630 return
   631 }

Легко заметить, что тут «паника» используется ровно как исключение. Даже видно, что «паника» «пробрасывается» выше, если это не та ошибка, которую мы стремились подавить.

Причём «паника» она, как исключения, типизирована. Например: panic(42), panic(«Hello»), можно передавать структуры. То есть можно по типу и содержимому может что-то рассказывать об ошибке.

ведь обращение за пределы массива однозначно свидетельствует о логической ошибке в программе. зачем ей вообще работать с логической-то ошибкой?

В Пайтоне я бы сделал как-то так:

try:
    cached = cache[somekey]
except IndexError:
    cached = None

Где тут логическая ошибка?

Или вот в моём примере про деление на ноль (вызов Quo), это такая страшная ситуация, что программа должна валиться и выходить?

Вообще, когда я читал документацию, у меня тоже сложилось впечатление, что «паника» — это unrecoverable fatal, теперь, когда я побольше почитал код модулей, я начинаю понимать, что «паника» это аналог исключений и относиться к ней нужно соответственно. Но обрабатывать её («панику») не слишком-то удобно.