panic и recover
panic — это не исключение
Первое что нужно понять: panic — не замена try/catch. Джуны из Java или Python иногда пытаются использовать panic/recover как механизм исключений. Это антипаттерн.
В Go:
- error — ожидаемые ситуации: файл не найден, сеть недоступна, невалидные данные
- panic — неожиданные ситуации: баги программы, нарушение инвариантов, «это никогда не должно случиться»
// error — правильно для ожидаемых ошибок:
func openFile(path string) (*os.File, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("openFile: %w", err)
}
return f, nil
}
// panic — правильно для багов:
func mustPositive(n int) int {
if n <= 0 {
panic(fmt.Sprintf("mustPositive: ожидалось n > 0, получили %d", n))
}
return n
}Как работает panic
panic останавливает нормальное выполнение функции, раскручивает стек вызовов и выполняет все defer по пути:
func c() {
panic("что-то сломалось")
}
func b() {
defer fmt.Println("defer в b") // выполнится при раскрутке
c()
fmt.Println("эта строка не выполнится")
}
func a() {
defer fmt.Println("defer в a") // тоже выполнится
b()
}
func main() {
a()
}
// Вывод:
// defer в b
// defer в a
// goroutine 1 [running]:
// main.c(...)
// panic: что-то сломалосьЭто важно: defer всегда выполняется, даже при панике. Именно поэтому defer file.Close() и defer mu.Unlock() надёжны — ресурсы освободятся даже если произойдёт паника.
recover: поймать панику
recover() останавливает раскрутку стека и возвращает значение, переданное в panic. Работает только внутри defer:
func safeDiv(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
result = a / b // паника при b == 0
return
}
func main() {
result, err := safeDiv(10, 2)
fmt.Println(result, err) // 5 <nil>
result, err = safeDiv(10, 0)
fmt.Println(result, err) // 0 recovered: runtime error: integer divide by zero
}Конструкция if r := recover(); r != nil — стандартный паттерн. recover() возвращает nil если паники не было.
Паттерн: безопасная горутина
Паника в горутине без recover убивает всю программу. Для долгоживущих сервисов — катастрофа:
// Опасно: паника в горутине роняет сервис
go func() {
processRequest(req) // а вдруг паника?
}()
// Безопасно: оборачиваем в recover
func safeGo(fn func()) {
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("recovered panic: %v\n%s", r, debug.Stack())
}
}()
fn()
}()
}
safeGo(func() {
processRequest(req)
})Веб-фреймворки (gin, echo, chi) делают это автоматически через middleware — каждый HTTP запрос обрабатывается в recover, чтобы паника в хендлере не ронала сервер.
Конвертация паники в error
Иногда библиотека использует panic внутри, но публичный API должен возвращать error. Паттерн конвертации:
// Внутренняя функция может паниковать
func parseInternal(data []byte) int {
if len(data) == 0 {
panic("пустые данные")
}
// ...
return 42
}
// Публичная функция конвертирует панику в error
func Parse(data []byte) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("parse failed: %v", r)
}
}()
result = parseInternal(data)
return
}Так устроен encoding/json — внутри он паникует, но публичные функции всегда возвращают error.
Когда panic оправдан
// 1. Невозможная ситуация — нарушение инварианта
switch direction {
case North, South, East, West:
move(direction)
default:
panic(fmt.Sprintf("неизвестное направление: %v", direction))
}
// 2. Must-функции: инициализация которая обязана успешной
var tmpl = template.Must(template.ParseFiles("index.html"))
// template.Must паникует если шаблон не распарсился — на старте приложения это ок
// 3. Явно неверное использование API
func NewBuffer(size int) *Buffer {
if size <= 0 {
panic("NewBuffer: size должен быть > 0")
}
return &Buffer{data: make([]byte, size)}
}Паттерн Must* встречается в стандартной библиотеке: regexp.MustCompile, template.Must. Они паникуют при ошибке — используй их только для инициализации на старте программы, никогда в горячих путях.
Антипаттерны
// ПЛОХО: panic вместо error для обычных ошибок
func getUser(id int) *User {
user, ok := db[id]
if !ok {
panic("user not found") // вызывающий не может это обработать!
}
return user
}
// ХОРОШО:
func getUser(id int) (*User, error) {
user, ok := db[id]
if !ok {
return nil, fmt.Errorf("getUser(%d): %w", id, ErrNotFound)
}
return user, nil
}
// ПЛОХО: пустой recover — проглатывает панику бесследно
defer func() {
recover() // что произошло? никто не знает
}()
// ХОРОШО: логируй и возвращай ошибку
defer func() {
if r := recover(); r != nil {
log.Printf("panic: %v\n%s", r, debug.Stack())
err = fmt.Errorf("internal error: %v", r)
}
}()Итого: error vs panic
| error | panic | |
|---|---|---|
| Когда | ожидаемые сбои | баги, нарушение инвариантов |
| Обработка | if err != nil | defer/recover |
| Распространение | явный возврат | автоматически вверх по стеку |
| Горутины | независимо | убивает программу |
| В библиотеках | всегда | только для Must* функций |
Хорошее практическое правило: если ты сомневаешься — используй error. panic нужен редко и по очень конкретным причинам.
Когда вызывается panic, Go начинает раскручивать стек вызовов, выполняя все зарегистрированные defer перед завершением программы.