В мире Go-разработки есть два типа ошибок: обычные, которые мы обрабатываем с помощью error, и настоящие «чрезвычайные ситуации» — паники (panic). Если error — это вежливое «извините, что-то пошло не так», то panic — это громкий крик рантайма, после которого программа, если её не спасти, немедленно прекращает работу. Давайте разберёмся, что скрывается за страшным сообщением «panic: runtime error», почему это происходит и как с этим жить, не теряя рассудка и данных.
Что такое panic в Go? Анатомия катастрофы
Panic — это встроенный механизм языка Go для обработки исключительных ситуаций, которые программа не может или не должна обрабатывать в обычном потоке выполнения. Это не баг, а особое состояние рантайма. Когда возникает panic, выполнение функции немедленно останавливается, начинается процесс «раскрутки стека» (unwinding) — завершения всех отложенных вызовов (defer), и, если panic не был восстановлен, программа завершается с ненулевым кодом выхода.
Ключевое отличие от error: Error — это часть логики программы, ожидаемое нештатное состояние (файл не найден, сетевой сбой). Panic — это неожиданное нарушение инвариантов языка: обращение к nil-указателю, выход за границы массива, работа с закрытым каналом.
Типичные причины panic: откуда ждать беды
1. Nil pointer dereference
Классика жанра. Попытка обратиться к полю или методу у nil-указателя.
var s *SomeStruct
fmt.Println(s.Field) // panic: runtime error: invalid memory address or nil pointer dereference
2. Index out of range
Выход за пределы массива или слайса.
arr := []int{1, 2}
fmt.Println(arr[5]) // panic: runtime error: index out of range [5] with length 2
3. Работа с закрытым каналом
Отправка данных в закрытый канал.
ch := make(chan int)
close(ch)
ch <- 1 // panic: send on closed channel
4. Вызов panic() вручную
Разработчик может сам инициировать панику с помощью встроенной функции panic(). Это оправдано в крайне редких случаях, например, при обнаружении невозможного состояния в глубоко вложенной логике, которое свидетельствует о серьёзной ошибке в архитектуре.
Recover: спасательный круг в море паники
Go предоставляет механизм восстановления — recover(). Это встроенная функция, которая останавливает раскрутку стека и возвращает значение, переданное в panic(). Важнейшее правило: refer работает только внутри отложенной функции (defer).
func safeFunction() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Восстановлено после паники:", r)
// Здесь можно залогировать ошибку, отправить метрику, etc.
}
}()
dangerousFunction() // может запаниковать
}
Важно: Используйте recover осознанно, только на границах вашего кода (горутины, обработчики HTTP-запросов, main). Восстановление в середине бизнес-логики часто маскирует корневые проблемы и приводит к нестабильному состоянию программы.
Философия обработки: когда паниковать, а когда нет?
Сообщество Go выработало конвенции:
- Не используйте panic для обработки ожидаемых ошибок. Для этого есть
error. - В библиотеках и пакетах избегайте паники. Возвращайте ошибки вызывающему коду. Паника в библиотеке — это невежливо по отношению к пользователю, который может не ожидать её.
- В main-функции или корневых горутинах используйте recover. Это последний рубеж, чтобы программа завершилась gracefully (записала логи, закрыла соединения) даже при фатальной ошибке.
- Panic допустима в init()-функциях или если программа не может корректно стартовать (не найден критический конфиг, не удалось подключиться к БД). Лучше упасть при старте, чем работать в неконсистентном состоянии.
FAQ: Часто задаваемые вопросы о panic в Go
Чем panic в Go отличается от исключений (exceptions) в Java/Python?
В Go нет традиционной модели исключений с try/catch/finally. Panic не предназначена для контроля потока выполнения. Это именно аварийное завершение, которое должно быть редким. Отсутствие исключений заставляет разработчиков явно обрабатывать ошибки через возвращаемые значения, что делает код более предсказуемым.
Можно ли отловить panic в другой горутине?
Нет, panic распространяется только в рамках стека той горутины, в которой возникла. Если горутина запаниковала и в ней нет recover, она завершится, не затрагивая другие горутины. Поэтому критически важно иметь recover на верхнем уровне каждой горутины, запускаемой на длительное время.
Что происходит с defer при panic?
Это одно из главных преимуществ механизма defer. Все отложенные вызовы (defer) в функции и в функциях выше по стеку будут гарантированно выполнены в процессе раскрутки стека перед завершением программы. Это позволяет безопасно освобождать ресурсы (закрывать файлы, разблокировать мьютексы) даже в случае паники.
Как дебажить panic в продакшене?
- Всегда логируйте значение, возвращаемое recover().
- Используйте встроенный сборщик дампов (core dumps) или трассировку стека. Вывод panic в консоль содержит полный стек вызовов — сохраняйте его.
- Настройте мониторинг (например, Sentry, собственные логи) для автоматического сбора информации о паниках.
Panic — это плохо?
Не всегда. Panic — это индикатор серьёзной ошибки, которую нельзя было проигнорировать. В разработке она помогает быстро находить места, где нарушаются базовые инварианты. В продакшене задача — не допустить её до конечного пользователя, грамотно восстановиться, залогировать инцидент и исправить корневую причину в коде.