Модуль
1Ошибки: основы2Оборачивание ошибок3Свои типы ошибок4panic и recover← вы здесь
Урок 4~12 минут

panic и recover

panic — это не исключение

Первое что нужно понять: panic — не замена try/catch. Джуны из Java или Python иногда пытаются использовать panic/recover как механизм исключений. Это антипаттерн.

В Go:

  • error — ожидаемые ситуации: файл не найден, сеть недоступна, невалидные данные
  • panic — неожиданные ситуации: баги программы, нарушение инвариантов, «это никогда не должно случиться»
go
// 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 по пути:

go
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:

go
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
// Опасно: паника в горутине роняет сервис
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. Паттерн конвертации:

go
// Внутренняя функция может паниковать
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 оправдан

go
// 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. Они паникуют при ошибке — используй их только для инициализации на старте программы, никогда в горячих путях.


Антипаттерны

go
// ПЛОХО: 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

errorpanic
Когдаожидаемые сбоибаги, нарушение инвариантов
Обработкаif err != nildefer/recover
Распространениеявный возвратавтоматически вверх по стеку
Горутинынезависимоубивает программу
В библиотекахвсегдатолько для Must* функций

Хорошее практическое правило: если ты сомневаешься — используй error. panic нужен редко и по очень конкретным причинам.

panic — для ситуаций, которые не должны происходить никогда (баги программы). error — для ожидаемых сбоев (нет файла, нет сети). recover работает ТОЛЬКО внутри defer. Не используй panic как исключения.

Когда вызывается panic, Go начинает раскручивать стек вызовов, выполняя все зарегистрированные defer перед завершением программы.

Стек вызовов
main()
defer fmt.Println("main defer")
a()
defer fmt.Println("a defer")
b()
defer cleanup()
c()
Вывод
// нажмите Запустить
Исходный код
func main() { defer fmt.Println("main defer") a() } func a() { defer fmt.Println("a defer") b() } func b() { defer cleanup() c() } func c() { panic("something went wrong") }
🎯
Миссия 1 из 4
В каком случае оправдано использовать panic вместо error?