Шпаргалка по обработке ошибок в Go

Краткий справочник по обработке ошибок в Go. Всё на одной странице: создание, оборачивание, проверка, выбор инструмента под ситуацию. Плюс антипаттерны, которые стоит ловить на ревью. Здесь только «как». Объяснение «почему так» есть в полной статье про обработку ошибок. Если вы только начинаете изучать язык, весь маршрут есть в дорожной карте Go для начинающих.

Создание ошибок

// Статическое сообщение
err := errors.New("файл не найден")

// Сообщение с параметрами
err := fmt.Errorf("файл %s не найден", filename)

// Sentinel-ошибка уровня пакета
var ErrNotFound = errors.New("not found")

// Типизированная ошибка с данными
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("валидация поля %s: %s", e.Field, e.Message)
}

Оборачивание: %w и %v

// %w оборачивает: оригинал сохраняется, errors.Is его найдёт
return fmt.Errorf("чтение конфига: %w", err)

// %v форматирует: текст останется, связь с оригиналом потеряется
return fmt.Errorf("чтение конфига: %v", err)
ГлаголСвязь с оригиналомКогда использовать
%wСохраняется, работают errors.Is/errors.AsПо умолчанию
%vТеряетсяСкрыть деталь реализации от вызывающего кода

Контекст в обёртке отвечает на вопросы «что делали» и «с какими данными»:

return fmt.Errorf("получение пользователя id=%d: %w", id, err)

Проверка: errors.Is и errors.As

// errors.Is: проверка на конкретное значение (sentinel)
if errors.Is(err, sql.ErrNoRows) {
    return nil, ErrNotFound
}

// errors.As: извлечение типизированной ошибки
var ve *ValidationError
if errors.As(err, &ve) {
    log.Printf("поле %s: %s", ve.Field, ve.Message)
}

Обе функции рекурсивно проходят всю цепочку обёрток. Прямое сравнение err == sql.ErrNoRows сломается на первой же обёртке через %w. Не используйте его.

Несколько ошибок: errors.Join

// Собрать все ошибки валидации, а не падать на первой
func validate(o Order) error {
    var errs []error
    if o.CustomerID == 0 {
        errs = append(errs, errors.New("не указан customer_id"))
    }
    if len(o.Items) == 0 {
        errs = append(errs, errors.New("заказ без товаров"))
    }
    return errors.Join(errs...) // nil, если все ошибки nil
}

// Закрыть несколько ресурсов одним выражением
func (app *App) Close() error {
    return errors.Join(app.db.Close(), app.cache.Close())
}

errors.Is и errors.As работают и с объединёнными ошибками: проверяется каждая ошибка в группе.

Ситуация → инструмент

СитуацияИнструмент
Создать ошибку со статичным текстомerrors.New
Создать ошибку с параметрамиfmt.Errorf
Добавить контекст и сохранить оригиналfmt.Errorf с %w
Проверить «та ли это ошибка»errors.Is
Достать данные из ошибкиerrors.As + типизированная ошибка
Собрать несколько ошибок в однуerrors.Join
Ошибки из группы горутинerrgroup (отдельный разбор)
Программа не может продолжать работуpanic (только инициализация и нарушение инвариантов)

Sentinel или типизированная ошибка

КритерийSentinel (var ErrX = errors.New)Типизированная (struct)
Что важно вызывающемуСам факт: «не найдено», «нет доступа»Данные: какое поле, какой лимит
Проверкаerrors.Iserrors.As
Пример из stdlibsql.ErrNoRows, io.EOF*os.PathError, *json.SyntaxError

Маппинг ошибок в HTTP-ответ

func handleError(w http.ResponseWriter, err error) {
    var ve *ValidationError
    if errors.As(err, &ve) {
        http.Error(w, ve.Error(), http.StatusBadRequest)
        return
    }
    if errors.Is(err, ErrNotFound) {
        http.Error(w, "not found", http.StatusNotFound)
        return
    }
    log.Printf("внутренняя ошибка: %v", err)
    http.Error(w, "internal server error", http.StatusInternalServerError)
}

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

ПлохоПочемуКак надо
err == sql.ErrNoRowsЛомается при оборачиванииerrors.Is(err, sql.ErrNoRows)
strings.Contains(err.Error(), "not found")Текст ошибки может изменитьсяerrors.Is / errors.As
return err без контекстаВ логах непонятно, откуда ошибкаfmt.Errorf("операция X: %w", err)
Залогировать и вернуть ошибкуОдна ошибка в логах несколько разЛибо обработать, либо передать выше
json.Unmarshal(data, &v) без проверкиТихий багПроверять; намеренное игнорирование показывать как _ =
panic(err) в бизнес-логикеnet/http панику восстановит, но соединение оборвётся. Паника в горутине убьёт весь процессВозвращать error
Вернуть и значение, и ошибкуВызывающий может использовать частичный результатПри ошибке возвращать нулевое значение. Исключение: осмысленный частичный результат, как у io.Reader

Непроверенные ошибки ловит линтер errcheck. Он входит в golangci-lint. Включите его в CI.

Тестирование ошибок

// Sentinel: проверяем через errors.Is
if !errors.Is(err, ErrNotFound) {
    t.Errorf("ожидали ErrNotFound, получили: %v", err)
}

// Типизированная: извлекаем через errors.As
var ve *ValidationError
if !errors.As(err, &ve) {
    t.Fatalf("ожидали ValidationError, получили: %v", err)
}

Текст ошибки через err.Error() в тестах не проверяйте. Проверяйте тип и семантику.

Итог

Минимальный рабочий набор: fmt.Errorf с %w для оборачивания, errors.Is для sentinel-ошибок, errors.As для типизированных, errors.Join для групп. Остальное в таблице «ситуация → инструмент» выше. Если по какому-то пункту хочется понять «почему именно так», открывайте полный разбор обработки ошибок.

Сохраните шпаргалку себе. Новые шпаргалки и разборы я анонсирую в Telegram-канале «Бруяко: код и команда».


Теги: