ПрограммированиеBackend разработчик

Расскажите, как устроена работа с ошибками через package error wrapping в Go: что такое error wrapping, почему это важно и как правильно реализовать вложение ошибок?

Проходите собеседования с ИИ помощником Hintsage

Ответ.

История вопроса:

До Go 1.13 ошибка была простым интерфейсом. Для дополнительного контекста ошибки часто создавали свои типы, но структурированного механизма вложения, как в modern Java или C#, не было. С появлением Go 1.13 был введён стандартный способ обёртывания (wrapping) ошибок через функции fmt.Errorf и выявления корневой причины (errors.Is/errors.As).

Проблема:

В сложных приложениях, где ошибка может возникать на разных уровнях стека, важно не терять контекст при возврате ошибки из глубины кода. Иначе дебаг становится трудным и невозможно понять, где в цепочке произошёл сбой.

Решение:

Go предлагает оборачивать ошибки в новые объекты ошибки, которые содержат причину. Для этого используется %w в fmt.Errorf, а для проверки глубинных причин — errors.Is или errors.As из пакета errors.

Пример кода:

import ( "errors" "fmt" ) var ErrNotFound = errors.New("not found") func getData() error { return fmt.Errorf("service database: %w", ErrNotFound) } func main() { err := getData() if errors.Is(err, ErrNotFound) { fmt.Println("Обнаружена причина: не найдено") } else { fmt.Println("Другая ошибка:", err) } }

Ключевые особенности:

  • Использование %w для вложения ошибок через fmt.Errorf.
  • Выявление вложенных причин ошибок через errors.Is и errors.As.
  • Совместимость с кастомными типами ошибок при вложении and unwrap.

Вопросы с подвохом.

Какой оператор форматирования нужен для обёртывания ошибок через fmt.Errorf?

Верно использовать %w, а не %v — только %w даёт поддержку unwrap.

Пример кода:

fmt.Errorf("ошибка: %w", err)

Можно ли вручную создавать цепочки ошибок без fmt.Errorf и всё равно их выявлять через errors.Is?

Нет, нужно реализовать интерфейс Unwrap, иначе стандартные функции не развернут цепочку.

Пример кода интерфейса:

type wrappedError struct { msg string err error } func (w wrappedError) Error() string { return w.msg + ": " + w.err.Error() } func (w wrappedError) Unwrap() error { return w.err }

Возвращают ли ошибки значения nil после wrapping, если вложенная ошибка была nil?

Нет, если вложенная ошибка nil, то обёрнутая ошибка тоже nil только при корректном использовании. Но если сделать wrapper-структуру вручную, где поле ошибки nil, будет не nil. Это часто приводит к путанице при проверке.

Типовые ошибки и анти-паттерны

  • Не использовать %w, а только %v — теряется возможность анализа цепочки.
  • Не реализовывать Unwrap() для своих структур ошибок.
  • Не делать проверку через errors.Is/As, а только сравнивать ошибку напрямую.
  • Создание новых ошибок без причины (потеря контекста).

Пример из жизни

Негативный кейс

В приложении просто возвращали новую ошибку с текстом на каждом уровне:

return errors.New("ошибка записи в БД")

Плюсы:

  • Просто и быстро.

Минусы:

  • Невозможно различить, что ошибка на самом деле — это "not found" из глубины, и сломать бизнес-логику наверху.
  • Потеря информации о стеке и источнике ошибки.

Позитивный кейс

Использовали обёртывание ошибок с %w и анализ через errors.Is:

if errors.Is(err, ErrNotFound) { return fmt.Errorf("ошибка уровня сервиса: %w", err) }

Плюсы:

  • Корректно определяется причина на любом уровне.
  • Проще дебажить, всегда виден оригинальный context.

Минусы:

  • Требует понятия, как работает wrapping и квалифицированного написания ошибок.