#14 golang panic Восстановление после паники - "перехват" исключетельной ситуации

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

Рассмотрим пример:

func main() {
	myFunc()
}

func myFunc() string {
	fmt.Println("Тело функции")

	defer func() {
		// Проверим была ли ошибка
		if err := recover(); err != nil {
			// если была, то обработаем:
			fmt.Println("Мы упали с сообщением:",
				err)
		}
	}()
	fmt.Println("Полезная работа")
	// вызываем падение:
	panic("Пора отдыхать!")
	return "Есть результат!"
}

-- выполнив это код, получим распечатку:

Тело функции
Полезная работа
Мы упали с сообщением: Пора отдыхать!

-- в примере выше мы использовали блок if с инициаллизацией, где, собственно, с помощью вызова recover() останавливаем падение программы и получаем информацию о брошенной ранее панике.

Затем мы проверяем значение ошибки на непустоту, и если оно непусто, то выводим текстовое сообщение полученной из данных об ошибке (панике).

Как это работает

Паника останавливает выполнение и начинает раскручивать стэк вызовов в обратную сторону, с тем уточнением, что назначенная отложенная функция все-таки вызывается, даже в состоянии паники (что и иллюстрирует пример выше).

Как не надо использовать паники

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

В golang паники ловят только для того, чтобы избежать падение горутины, которая должна обрабатывать еще и другие запросы, кроме текущего, который завершился ошибкой, но не для управления логикой.

Так, самим панику следует выбрасывать только когда всё действительно плохо (для так называемых "неустранимых ошибок"), а в обычных ситуациях, для ожидаемых проблем напр. некорретного пользовательского ввода просто возвращать признак ошибки, напр. как второе значение функции.

Паника в функции восстановления

Функция восстановления может и сама бросить панику, но следующая по стеку отложенная функция (если она есть, помним, что они складываются в стэк) может перехватить и её:

func main() {
	myFunc()
}

func myFunc() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("Обработчик 1:", err)
		}
	}()
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("Обработчик 2:", err)
			panic("Вторая паника!")
		}
	}()
	fmt.Println("Some userful work")
	panic("Первая паника!")
	return
}

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

Отдельный обработчик паник

Обработчик паники можно вынести в отдельную функцию и подключать в любой функции, где мы не хотим упасть из-за паники, а хотим обработать ошибку и восстановить выполнение, например для main():

import (
	"fmt"
)

// Обработчик паник
func panicHandler() {
	// Проверим была ли ошибка
	if err := recover(); err != nil {
		// если была, то обработаем:
		fmt.Println(
			"Мы упали c сообщением: \n",
			err)
	}
}

func main() {
	// отложенная функция восстановления
	// для обработки паники
	defer panicHandler()
	// самостоятельно вызываем критическую ошибку:
	panic("Демонстрационная паника!")
}

-- если запустим это код, получим:

Мы упали c сообщением: 
 Демонстрационная паника!

Видео-материалы

Key Words for FKN + antitotal forum (CS VSU):