Panic і recover¶
07-errors.md розглядає основний механізм обробки помилок у Go —
повернення error. Ця стаття охоплює інший: panic (паніка) і recover
(відновлення) — вони існують для невеликого набору випадків, де звичайне
повернення помилки не підходить.
Головне правило від початку: не використовуйте panic для звичайних помилок.
Використовуйте його для помилок програміста (неможливі стани, порушення контракту)
та для невідновлюваних умов часу виконання. Для всього іншого — повертайте error.
Що робить panic¶
Виклик panic(v) зупиняє нормальне виконання функції і починає
розмотування стеку:
- Відкладені виклики поточної функції виконуються у порядку LIFO.
- Функція повертається до свого викликача.
- Виконуються відкладені виклики викликача.
- Викликач повертається до свого викликача.
- Це продовжується доти, доки:
- відкладена функція не викличе
recover()і не зупинить розмотування, або - panic не досягне
mainі не завершить програму з виведенням стека викликів.
Помилки часу виконання — розіменування nil-вказівника, вихід за межі зрізу, ділення цілого числа на нуль, відправлення в закритий канал — усі ініціюють неявний panic.
package main
import "fmt"
func main() {
defer fmt.Println("deferred in main")
panic("boom")
}
// output:
// deferred in main
// panic: boom
//
// goroutine 1 [running]:
// main.main()
// .../main.go:7 +0x...
// exit status 2
Відкладений fmt.Println виконався (розмотування panic все одно виконує відкладені
виклики), а потім програма аварійно завершилась.
Що робить recover¶
recover() — це вбудована функція, яка щось робить лише всередині відкладеної
функції. В будь-якому іншому місці вона повертає nil і нешкідлива.
- Під час нормального виконання (без активного panic):
recoverповертаєnil. - Під час активного panic:
recoverповертає значення, передане доpanic, і зупиняє розмотування в поточній функції. Ця функція нормально повертається до свого викликача.
package main
import "fmt"
func safe() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered:", r)
}
}()
panic("oops")
}
func main() {
safe()
fmt.Println("main keeps going")
}
// output:
// recovered: oops
// main keeps going
Конструкція defer func() { ... }() — це універсальний патерн:
- Відкладений виклик повинен бути функцією (не можна зробити
defer recover()напряму — технічно можна, але це майже завжди помилка). recover()повинен викликатись з функції, яка сама безпосередньо є відкладеною. Якщо відкладена функція викликає іншу допоміжну, а та викликаєrecover(), panic перехоплено не буде — на тій глибині panic вже не активний.
func wrong() {
defer func() { // це відкладена функція
helper() // helper викликається ВІД відкладеної функції
}()
panic("boom")
}
func helper() {
if r := recover(); r != nil { // ніколи не спрацює — неправильний фрейм
fmt.Println("won't print")
}
}
Виправлення просте: викликайте recover безпосередньо всередині літерала відкладеної функції, а не з допоміжної функції.
Повний приклад розмотування¶
package main
import "fmt"
func main() {
f()
fmt.Println("returned normally from f")
}
func f() {
defer func() {
if r := recover(); r != nil {
fmt.Println("recovered in f:", r)
}
}()
fmt.Println("calling g")
g(0)
fmt.Println("returned normally from g")
}
func g(i int) {
if i > 3 {
fmt.Println("panicking!")
panic(fmt.Sprintf("%v", i))
}
defer fmt.Println("defer in g", i)
fmt.Println("printing in g", i)
g(i + 1)
}
Вивід:
calling g
printing in g 0
printing in g 1
printing in g 2
printing in g 3
panicking!
defer in g 3
defer in g 2
defer in g 1
defer in g 0
recovered in f: 4
returned normally from f
Зверніть увагу:
- Кожен
deferна шляху розмотування виконався. - Рядок «returned normally from g» у
gне виведено — panic пропустив його. - recover у
fзупинив розмотування;mainпобачила, щоfповернулась нормально, і вивела свій останній рядок.
Коли використовувати panic — три законні випадки¶
- Помилка програміста, яку система типів не може зловити. Виклик
divide(0, 0), індексування зрізу, в якому щойно підтверджено 3 елементи, на позиції5, звернення до поля структури, яке мало бути задане. Це баги; правильна поведінка — аварійно завершитись і виявити їх.
Стандартна бібліотека надає один допоміжний засіб для цього:
-
Невідновлювана умова часу виконання. Програма буквально не може продовжувати роботу. Наприклад, втрата необхідної конфігурації при запуску.
-
Як внутрішній механізм розмотування, з відновленням на межі пакету. Стандартна бібліотека так і робить —
encoding/jsonпанікує всередині під час рекурсивного обходу і відновлюється на верхівціMarshal/Unmarshal, потім повертає звичайнийerrorвикликачу.
Зовнішній API все одно повертає error. Викликач ніколи не бачить panic.
Коли не використовувати panic¶
- Очікуваний випадок помилки («файл не існує», «введення невалідне»,
«мережевий виклик не вдався»). Поверніть
error. - Щоб заощадити на перевірках помилок. Повторювані блоки
if err != nil— це ідіома, а не запах коду. - Щоб імітувати виключення. Дизайн Go свідомо відкидає виключення. Не варто їх винаходити заново.
З досвіду Python: Python використовує виключення як для звичайних помилок, так і для помилок програміста (
KeyError,IndexError,TypeError). Go розділяє відповідальність —errorдля звичайних збоїв, panic лише для «такого не повинно траплятись». Перехоплення panic через recover — приблизний аналогexcept:на межі процесу, а не звичайний інструмент управління потоком.
recover на межі горутини¶
(Пропустіть цей розділ, якщо ви ще не стикалися з горутинами — це легковагові конкурентні задачі Go, розглядаються в окремій темі. Ключове слово go запускає функцію як горутину: go work() виконує work паралельно з викликачем, не блокуючи його.)
Panic розмотує лише поточну горутину (goroutine). Якщо горутина панікує і ніщо в її стеку не виконує recover, вся програма завершується аварійно — навіть якщо всі інші горутини були справні.
Тому: якщо ви запускаєте горутину, яка може панікувати, сама горутина повинна мати recover на верхівці (або ви маєте бути впевнені, що горутина не може панікувати).
go func() {
defer func() {
if r := recover(); r != nil {
log.Println("worker crashed:", r)
}
}()
runJob()
}()
Це часте джерело виробничих збоїв: необроблений panic у фоновій горутині завалює весь процес.
Значення panic може бути будь-якого типу¶
panic приймає any. Можна панікувати з рядком, помилкою, власною структурою —
будь-чим.
Угода: якщо панікуєте з помилкою, можна виконати recover і повернути її як помилку. Частина r.(error) — це перевірка типу (type assertion) — читайте як «якщо r насправді містить error, дай мені його як e, і встанови ok в true; інакше ok дорівнює false». Перевірки типу отримають власний розгляд в темі інтерфейсів; поки що сприймайте цю конструкцію як «розпакувати значення any у відомий тип, безпечно».
func safeRun(f func()) (err error) {
defer func() {
if r := recover(); r != nil {
if e, ok := r.(error); ok {
err = e
} else {
err = fmt.Errorf("panic: %v", r)
}
}
}()
f()
return nil
}
Короткий довідник¶
| Потреба | Запис |
|---|---|
| Аварійне завершення при неможливому стані | panic("invariant violated: ...") |
| Перехопити panic на межі | defer func() { if r := recover(); r != nil { ... } }() |
| Перетворити panic на помилку | recover у іменоване повернення: defer func() { if r := recover(); r != nil { err = ... } }() |
| Захистити фонову горутину | загорніть її тіло в defer recover() і залогуйте значення |
| Просто повернути помилку замість цього | завжди ваш перший вибір — дивіться 07-errors.md |