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

Что такое упаковка ошибок?

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

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

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

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

Упаковка ошибок (Error Wrapping) в Go

Упаковка ошибок — это идиоматический подход в Go, позволяющий создавать цепочки ошибок, где каждая последующая ошибка "оборачивает" предыдущую, добавляя контекстную информацию. Этот механизм появился в Go 1.13 и стал стандартным способом обработки ошибок, сохраняя при этом полную историю их возникновения.

Как работает упаковка ошибок

Основная идея заключается в том, что вместо простого возврата новой ошибки мы "оборачиваем" исходную ошибку, добавляя к ней дополнительный контекст. Это реализуется с помощью функции fmt.Errorf() с директивой %w:

package main

import (
    "errors"
    "fmt"
)

func processFile(filename string) error {
    data, err := readFile(filename)
    if err != nil {
        // Упаковываем ошибку, добавляя контекст
        return fmt.Errorf("не удалось обработать файл %s: %w", filename, err)
    }
    
    result, err := parseData(data)
    if err != nil {
        // Дополнительная упаковка в другой функции
        return fmt.Errorf("ошибка парсинга: %w", err)
    }
    
    return saveResult(result)
}

func readFile(filename string) ([]byte, error) {
    if filename == "" {
        return nil, errors.New("имя файла не может быть пустым")
    }
    // ... чтение файла
    return []byte{}, nil
}

Преимущества упаковки ошибок

  • Сохранение полного стека ошибок — мы не теряем оригинальную ошибку, а только дополняем её контекстом
  • Возможность проверки конкретных ошибок — можно проверить, содержится ли в цепочке определённая ошибка
  • Улучшенная диагностика — при логировании видна вся цепочка возникновения проблемы
  • Совместимость с существующим кодом — стандартный интерфейс error остаётся неизменным

Функции для работы с упакованными ошибками

Go 1.13 представил три ключевые функции в пакете errors:

package main

import (
    "errors"
    "fmt"
)

var ErrFileNotFound = errors.New("файл не найден")

func main() {
    err := processFile("test.txt")
    
    // Проверка на конкретную ошибку в цепочке
    if errors.Is(err, ErrFileNotFound) {
        fmt.Println("Файл действительно не найден")
    }
    
    // Извлечение конкретной ошибки из цепочки
    var fileErr *FileError
    if errors.As(err, &fileErr) {
        fmt.Printf("Ошибка файла: %v\n", fileErr)
    }
    
    // Разворачивание ошибки для получения оригинальной
    originalErr := errors.Unwrap(err)
    if originalErr != nil {
        fmt.Printf("Оригинальная ошибка: %v\n", originalErr)
    }
}

Практические рекомендации по упаковке ошибок

  1. Добавляйте контекст на каждом уровне — каждая функция, которая получает ошибку и передаёт её выше, должна добавлять релевантный контекст:
func processUserData(userID string) error {
    data, err := fetchUserData(userID)
    if err != nil {
        return fmt.Errorf("processUserData: получение данных для пользователя %s: %w", userID, err)
    }
    
    // ... обработка данных
    return nil
}
  1. Используйте sentinel-ошибки с умом — создавайте публичные переменные ошибок для важных состояний:
var (
    ErrUserNotFound = errors.New("пользователь не найден")
    ErrInvalidToken = errors.New("невалидный токен")
    ErrDBConnection = errors.New("ошибка соединения с БД")
)
  1. Не упаковывайте ошибки без необходимости — если вы не добавляете полезный контекст, просто возвращайте ошибку как есть:
func validate(input string) error {
    if input == "" {
        // Просто возвращаем ошибку без упаковки
        return ErrEmptyInput
    }
    return nil
}

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

func processOrder(orderID string) error {
    order, err := db.GetOrder(orderID)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return fmt.Errorf("заказ %s не найден: %w", orderID, err)
        }
        return fmt.Errorf("ошибка БД при получении заказа: %w", err)
    }
    
    if err := validateOrder(order); err != nil {
        return fmt.Errorf("невалидный заказ %s: %w", orderID, err)
    }
    
    if err := processPayment(order); err != nil {
        var paymentErr *PaymentError
        if errors.As(err, &paymentErr) {
            return fmt.Errorf("платёжная ошибка для заказа %s: %w", orderID, paymentErr)
        }
        return fmt.Errorf("неизвестная ошибка платежа: %w", err)
    }
    
    return nil
}

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

Что такое упаковка ошибок? | PrepBro