← Назад к вопросам

Recover после panic

2.0 Middle🔥 121 комментариев
#Основы Go

Условие

Напишите функцию, которая выполняет переданную функцию и перехватывает panic, возвращая ошибку вместо падения программы.

Сигнатура

func safeExecute(fn func()) (err error)

Требования

  • Если fn выполнилась без panic - вернуть nil
  • Если fn вызвала panic - перехватить её и вернуть ошибку с сообщением из panic
  • Программа не должна падать

Пример

err := safeExecute(func() {
    panic("something went wrong")
})
// err.Error() == "recovered from panic: something went wrong"

err = safeExecute(func() {
    fmt.Println("all good")
})
// err == nil

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение: Recover после panic

Основная идея

Используем defer с recover() для перехвата паники:

  • defer выполняется при выходе из функции, включая panic
  • recover() возвращает значение, переданное panic
  • Если паники не было, recover() вернёт nil

Простая реализация

import (
    "fmt"
)

func safeExecute(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            // Преобразуем паику в ошибку
            switch v := r.(type) {
            case string:
                err = fmt.Errorf("recovered from panic: %s", v)
            case error:
                err = fmt.Errorf("recovered from panic: %w", v)
            default:
                err = fmt.Errorf("recovered from panic: %v", v)
            }
        }
    }()
    
    fn() // Выполняем функцию, может паниковать
    return nil
}

Как работает:

  1. defer регистрирует функцию восстановления
  2. fn() выполняется
  3. Если panic → defer выполняется автоматически
  4. recover() ловит паику и возвращает значение
  5. Преобразуем паику в error и возвращаем

Пример использования

func main() {
    // Тест 1: с паникой
    err := safeExecute(func() {
        panic("something went wrong")
    })
    fmt.Println(err) // recovered from panic: something went wrong
    
    // Тест 2: без паники
    err = safeExecute(func() {
        fmt.Println("all good")
    })
    fmt.Println(err) // <nil>
    
    // Тест 3: паника с error
    err = safeExecute(func() {
        panic(fmt.Errorf("custom error"))
    })
    fmt.Println(err) // recovered from panic: custom error
}

Версия с типом паники

func safeExecute(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            switch v := r.(type) {
            case string:
                // Паника был строкой
                err = fmt.Errorf("panic: %s", v)
            case error:
                // Паника был error интерфейсом
                err = fmt.Errorf("panic: %w", v)
            case int:
                // Паника было число
                err = fmt.Errorf("panic: %d", v)
            default:
                // Неизвестный тип
                err = fmt.Errorf("panic: %v (type: %T)", v, v)
            }
        }
    }()
    
    fn()
    return nil
}

Версия с logrus (для production)

import (
    "fmt"
    "github.com/sirupsen/logrus"
)

func safeExecute(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            logrus.WithField("panic", r).Error("Recovered from panic")
            
            switch v := r.(type) {
            case string:
                err = fmt.Errorf("recovered from panic: %s", v)
            case error:
                err = fmt.Errorf("recovered from panic: %w", v)
            default:
                err = fmt.Errorf("recovered from panic: %v", v)
            }
        }
    }()
    
    fn()
    return nil
}

Версия с возвращаемым результатом и ошибкой

func safeExecuteWithResult(fn func() (interface{}, error)) (result interface{}, err error) {
    defer func() {
        if r := recover(); r != nil {
            result = nil
            switch v := r.(type) {
            case string:
                err = fmt.Errorf("recovered from panic: %s", v)
            case error:
                err = fmt.Errorf("recovered from panic: %w", v)
            default:
                err = fmt.Errorf("recovered from panic: %v", v)
            }
        }
    }()
    
    return fn()
}

Версия для методов с receiver

type Handler struct {}

func (h *Handler) safeExecute(fn func()) (err error) {
    defer func() {
        if r := recover(); r != nil {
            // Логируем для отладки
            fmt.Printf("[%s] Recovered from panic: %v\n", h.String(), r)
            
            switch v := r.(type) {
            case string:
                err = fmt.Errorf("recovered from panic: %s", v)
            case error:
                err = fmt.Errorf("recovered from panic: %w", v)
            default:
                err = fmt.Errorf("recovered from panic: %v", v)
            }
        }
    }()
    
    fn()
    return nil
}

Версия с timeout

import (
    "context"
    "fmt"
    "time"
)

func safeExecuteWithContext(ctx context.Context, fn func(context.Context)) (err error) {
    defer func() {
        if r := recover(); r != nil {
            switch v := r.(type) {
            case string:
                err = fmt.Errorf("recovered from panic: %s", v)
            case error:
                err = fmt.Errorf("recovered from panic: %w", v)
            default:
                err = fmt.Errorf("recovered from panic: %v", v)
            }
        }
    }()
    
    fn(ctx)
    return nil
}

Тестирование

import "testing"

func TestSafeExecute(t *testing.T) {
    tests := []struct {
        name      string
        fn        func()
        shouldErr bool
        errMsg    string
    }{
        {
            name:      "no panic",
            fn:        func() { fmt.Println("ok") },
            shouldErr: false,
        },
        {
            name:      "string panic",
            fn:        func() { panic("error!") },
            shouldErr: true,
            errMsg:    "recovered from panic: error!",
        },
        {
            name:      "error panic",
            fn:        func() { panic(fmt.Errorf("custom")) },
            shouldErr: true,
            errMsg:    "recovered from panic: custom",
        },
        {
            name:      "int panic",
            fn:        func() { panic(42) },
            shouldErr: true,
            errMsg:    "recovered from panic: 42",
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := safeExecute(tt.fn)
            
            if tt.shouldErr && err == nil {
                t.Errorf("Expected error, got nil")
            }
            
            if !tt.shouldErr && err != nil {
                t.Errorf("Expected nil, got %v", err)
            }
            
            if err != nil && err.Error() != tt.errMsg {
                t.Errorf("Expected %q, got %q", tt.errMsg, err.Error())
            }
        })
    }
}

Когда использовать safeExecute

  • HTTP handlers: оборнуть обработчик для ловли паники
  • Горутины: обработать панику в горутине
  • Плагины: выполнить код плагина безопасно
  • User callbacks: выполнить callback без риска краша
  • Worker pools: каждый worker защищён от паники

Лучшие практики

  1. Всегда логируй панику:

    logrus.WithField("panic", r).WithField("stack", debug.Stack()).Error("Panic occurred")
    
  2. Различай типы паники:

    switch v := r.(type) {
    case string: // User panic("message")
    case error:  // User panic(error)
    default:     // Неожиданное
    }
    
  3. Не подавляй панику в критических местах: Если паника означает критическую ошибку, может быть лучше дать ей распространяться.

  4. Используй recover только в defer: recover() работает только в defer, иначе вернёт nil.

Это essential для production Go кода — защита от неожиданных паник.