Обработка ошибок в Go: try/catch и продвинутые техники

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Обработка ошибок в Go: try/catch и не только: продвинутый уровень

Разработчики, приходящие в Go из языков с исключениями (Java, Python, C#), часто задаются вопросом: «Где же здесь try/catch?». Ответ прост: в Go нет исключений в классическом понимании. Вместо этого язык предлагает явную, предсказуемую и эффективную модель обработки ошибок. На первый взгляд это может показаться шагом назад, но на деле это один из ключевых принципов философии Go — простота и надёжность. В этой статье мы не просто расскажем, как заменить try/catch, а разберём продвинутые техники работы с ошибками: от пользовательских типов до обёртывания и паники.

Почему в Go нет try/catch?

Отсутствие try/catch — осознанное архитектурное решение. Исключения часто приводят к неявным потокам выполнения, которые сложно отследить. Программа может «взорваться» в любом месте, если вы забыли обработать исключение. Go предлагает другой подход: ошибка — это просто значение (error is a value).

Это означает, что функция, которая может завершиться неудачей, возвращает значение ошибки как часть своего результата. Программист обязан проверить это значение. Такой подход делает поток ошибок видимым в коде и предотвращает ситуации, когда исключение «проглатывается» или обрабатывается слишком далеко от места возникновения.

// Типичная идиома Go
func ReadFile(path string) ([]byte, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("не удалось прочитать файл %s: %w", path, err)
    }
    return data, nil
}

// Использование
data, err := ReadFile("config.json")
if err != nil {
    log.Fatal(err) // или return err
}

Это и есть та самая замена try/catch. Вместо блока try { ... } catch (Exception e) { ... } мы используем if err != nil. Это не менее выразительно, но гораздо более явно.

Продвинутые техники: пользовательские ошибки и обёртывание

1. Пользовательские типы ошибок

Стандартный тип error — это интерфейс с единственным методом Error() string. Но для продвинутой обработки вам часто нужно больше: код ошибки, временный характер сбоя, дополнительные данные. Создайте свой тип, реализующий интерфейс error.

type MyError struct {
    Code    int
    Message string
    IsTemp  bool
}

func (e *MyError) Error() string {
    return fmt.Sprintf("код %d: %s (временная: %t)", e.Code, e.Message, e.IsTemp)
}

// Использование
func DoSomething() error {
    return &MyError{Code: 503, Message: "сервис недоступен", IsTemp: true}
}

// Проверка
err := DoSomething()
var myErr *MyError
if errors.As(err, &myErr) {
    if myErr.IsTemp {
        // Повторить попытку
    }
}

Совет: Используйте errors.As для безопасного приведения к вашему типу, а не прямое приведение типа (type assertion), чтобы избежать паники.

2. Обёртывание ошибок (Wrapping)

Одна из самых мощных фич Go 1.13+ — это возможность обёртывать ошибки с помощью fmt.Errorf и директивы %w. Это создаёт цепочку ошибок, которую можно распутать с помощью errors.Is и errors.As.

func OpenConfig() error {
    _, err := os.Open("config.yaml")
    if err != nil {
        // Оборачиваем ошибку, добавляя контекст
        return fmt.Errorf("открытие конфига: %w", err)
    }
    return nil
}

func LoadApp() error {
    err := OpenConfig()
    if err != nil {
        // Оборачиваем ещё раз
        return fmt.Errorf("загрузка приложения: %w", err)
    }
    return nil
}

// Где-то в main
err := LoadApp()
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("Файл конфигурации не найден")
}

Обратите внимание: errors.Is проходит по всей цепочке и находит, является ли какая-либо из ошибок os.ErrNotExist. Это намного чище, чем проверять строку ошибки!

3. Ошибки-маркеры (Sentinel Errors)

Это предопределённые глобальные переменные-ошибки, такие как io.EOF, sql.ErrNoRows. Они идеальны для сравнения с помощью errors.Is.

var ErrNotFound = errors.New("запись не найдена")

func GetUser(id int) (User, error) {
    // ...
    if user == nil {
        return User{}, ErrNotFound
    }
    return user, nil
}

// Проверка
user, err := GetUser(42)
if errors.Is(err, ErrNotFound) {
    // Создать нового пользователя
}

Panic и Recover: когда они нужны на самом деле?

В Go есть panic и recover, которые напоминают исключения. Однако их использование — это крайний случай, а не замена try/catch.

Паника означает, что программа находится в невосстановимом состоянии: например, выход за границы массива или разыменование nil-указателя. recover может перехватить панику, но используется почти исключительно для:

  • Graceful shutdown серверов (перехватить панику в горутине, чтобы не упал весь сервер).
  • Очистки ресурсов (например, закрытие соединения с БД перед выходом).

Никогда не используйте panic/recover для обычной обработки ошибок. Это нарушает поток выполнения и делает код непредсказуемым.

// Правильное использование: middleware для HTTP-сервера
func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("паника: %v", err)
                http.Error(w, "Внутренняя ошибка сервера", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

Best practices: как писать идиоматичный код

  1. Всегда проверяйте ошибки. Если функция возвращает error, вы обязаны его проверить. Игнорирование ошибок — главный

Блоги

Рекомендаций по книгам