суббота, 2 февраля 2019 г.

Эффективный Go: восстановление (recover)

Когда вызывается panic, в том числе неявно для ошибок времени выполнения (run-time errors), такие как индексация среза за пределами или сбой утверждения типа, оно немедленно останавливает выполнение текущей функции и начинает разматывать стек go-процедуры (goroutine), запуская любые отложенные функции по пути. Если это раскручивание достигает вершины стека go-процедуры, программа умирает. Тем не менее, можно использовать встроенную функцию restore для восстановления контроля go-процедуры и возобновления нормального исполнения.

Вызов restore останавливает раскручивание и возвращает аргумент переданный panic. Поскольку единственный код, который работает во время разматывания - это код, находящийся внутри отложенных функций, restore полезен только внутри отложенных функций.

Одно из применений restore - отключение отказавшей go-процедуры внутри сервера, не убивая другие исполняемые go-процедуры.

func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)
    }
}

func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Println("work failed:", err)
        }
    }()
    do(work)
}

В этом примере, если do(work) паникует, результат будет сохранен в логе и go-процедура будет выходить чисто, не нарушая другие. Нет необходимости делать что-либо еще в отсроченном закрытии; вызов restore полностью обрабатывает ситуацию.

Поскольку restore всегда возвращает nil, если не вызывается напрямую из отложенной функции, отложенный код может вызывать библиотечные процедуры, которые сами по себе используют panic и restore без сбоев. В качестве примера, отложенная функция в safeDo может вызывать функцию логирования перед вызовом restore, и этот код логирования будет работать без изменений состояния паники.

С нашим шаблоном восстановления, do функция (и все, что она вызывает) может выйти из любой плохой ситуации чисто, вызвав panic. Мы можем использовать эту идею для упрощения обработку ошибок в сложном программном обеспечении. Давайте посмотрим на идеализированную версию пакета regexp, который сообщает об ошибках парсиннга путем вызова panic с локальным типом ошибки. Вот определение Error, метода error и функции Compile.

// Error это тип ошибки разбора(парсинга); 
// он удовлетворяет error интерфейсу
type Error string
func (e Error) Error() string {
    return string(e)
}

// error это метод *Regexp, 
// который сообщает об ошибках разбора (парсинга),
// вызывая panic с Error.
func (regexp *Regexp) error(err string) {
    panic(Error(err))
}

// Compile возвращает разобранное представление
// регулярного выражения.
func Compile(str string) (regexp *Regexp, err error) {
    regexp = new(Regexp)
    // doParse будет вызывать panic 
    // если есть ошибка разбора.
    defer func() {
        if e := recover(); e != nil {
            // Очищаем возвращаемое значение.
            regexp = nil  
            // Будет повторная panic 
            // если нет ошибки разбора.  
            err = e.(Error) 
        }
    }()
    return regexp.doParse(str), nil
}

Если doParse паникует, блок восстановления установит возвращаемое значение равным nil - отложенные функции могут изменять именованные возвращаемые значения. Затем происходит проверка, в назначении err, что проблема была в ошибке разбора, утверждая что он имеет локальный тип Error. Если этого не произойдет, утверждение типа не будет выполнено, что приведет к ошибке во время выполнения, которая продолжит раскручивать стек, как будто ничего не прерывалось. Эта проверка означает, что если происходит что-то неожиданное, например запрос индекса вне пределов, код не будет работать, даже если мы используем panic и recover для обработки ошибок разбора.

С обработкой ошибок на месте, метод error (потому что это метод, связанный с типом, это нормально, даже естественно, чтобы он имел то же имя что и встроенный тип error) позволяет легко сообщать об ошибках разбора, не беспокоясь о раскручивании стека разбора вручную:

if pos == 0 {
    re.error("'*' недопустимо в начале выражения")
}

Хотя этот шаблон полезен, его следует использовать только внутри пакета. Parse превращает свои внутренние вызовы panic в значения error; он не подвергает panic своему клиенту. Это хорошее правило для использования в своей практике.

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


Читайте также:


Комментариев нет:

Отправить комментарий