defer: порядок выполнения и отличие ожидаемой ошибки от panic
Содержание курса
panic vs error: аварийная раскрутка стека против ожидаемого сбоя
error — это значение. Функция его возвращает, вызывающий код получает, смотрит на него и решает, что делать: повторить запрос, залогировать, вернуть выше. Программа продолжает работать в штатном режиме.
panic работает принципиально иначе. Когда Go встречает panic("что-то сломалось"), обычное выполнение текущей функции прекращается немедленно. Дальше происходит раскрутка стека вызовов: Go поднимается по цепочке вызвавших функций, и в каждой из них выполняет зарегистрированные defer. После того как стек раскручен до верхнего уровня, программа завершается аварийно и печатает сообщение о панике и трассировку.
Вот минимальный пример:
package main
import "fmt"
func riskyOp() {
panic("что-то сломалось")
}
func main() {
defer fmt.Println("defer в main сработал")
riskyOp()
}
Вывод будет выглядеть примерно так (конкретные пути и номера строк зависят от окружения):
defer в main сработал
panic: что-то сломалось
goroutine 1 [running]:
main.riskyOp(...)
.../main.go:...
main.main()
.../main.go:...
exit status 2
Посмотри на порядок: сначала при раскрутке стека выполняется defer fmt.Println, поэтому появляется строка defer в main сработал. Затем, поскольку panic не была перехвачена, программа печатает panic: что-то сломалось и трассировку и завершается. Это подтверждает, что defer срабатывает и во время раскрутки стека из-за panic, а не только при обычном return.
Почему нельзя использовать panic вместо error для обычных ошибок?
Во-первых, вызывающий код теряет контроль. Если функция вернула error, вызывающий может его проверить, обработать или передать выше. Если функция сделала panic, вызывающий код не может перехватить это обычным if err != nil — программа уже летит к завершению.
Во-вторых, это нарушает соглашение Go. Невалидный ввод, недоступная запись, несовпадение формата — всё это ожидаемые ситуации, для которых язык предоставляет error. panic предназначен для ситуаций, которые означают ошибку самого программиста или нарушение инварианта, который «никогда не должен произойти» при правильной логике программы.
Практически: если ты пишешь функцию divide(a, b int) и b == 0 — возвращай error. Если где-то внутри твоей структуры данных обнаруживается состояние, которое физически не может возникнуть при корректной работе кода, — panic уместен как сигнал о баге в самой программе, а не о внешней ситуации.
