handbookНаписать

languages/go/01-errors-and-wrapping/README.md · тема · 1 язык · 11 тестов

Ошибки как значения в Go (Errors and wrapping) ​

Ошибка в Go - обычное значение, которое можно обернуть контекстом, проверить по смыслу и сложить с другими: так импорт прайса сообщает обо всех плохих строках сразу, а вызывающий код решает, что делать, без разбора текста.

Проблема ​

Импорт прайса из CSV: строка sku;price, тысячи строк. Типичная первая версия:

go
for i, raw := range lines {
	sku, price, _ := strings.Cut(raw, ";")
	cents, err := strconv.Atoi(price)
	if err != nil {
		return nil, errors.New("bad price") // which line? which value?
	}
	// ...
}

if err != nil && strings.Contains(err.Error(), "bad price") { // matching on text

Импорт падает на первой ошибке, и менеджер правит файл по одной строке за загрузку. В сообщении нет ни номера строки, ни значения, а исходная ошибка strconv потеряна. Вызывающий код отличает "плохой файл" от "плохой строки" сравнением текста, и любая правка формулировки ломает обработку.

Решение ​

Go не прячет ошибки в исключения, поэтому ими можно управлять как данными:

  • Сентинелы ErrBadPrice, ErrEmptySKU, ErrDuplicateSKU, ErrBadHeader - именованные значения ошибок. С ними сравнивают через errors.Is, а не по тексту.
  • Свой тип RowError несёт номер строки и поле, а метод Unwrap() отдаёт причину. Достают его через errors.As.
  • %w в fmt.Errorf добавляет контекст и сохраняет цепочку. С Go 1.20 глаголов %w может быть несколько: ParseCents оборачивает и ErrBadPrice, и исходную *strconv.NumError, так что сработают обе проверки.
  • errors.Join складывает ошибки всех строк в одну. Import возвращает хорошие строки и общую ошибку; errors.Is по ней находит любую из вложенных, а Report разворачивает её в список сообщений.

Структура ​

каждая строка

пустой sku

%w: %w

повтор sku

нет заголовка

Import

ParseRow

RowError{sku} -> ErrEmptySKU

ParseCents

ErrBadPrice + strconv.NumError

RowError{price}

RowError{sku} -> ErrDuplicateSKU

errors.Join

import price list: ErrBadHeader

Две разные политики в одной функции: ошибка заголовка останавливает импорт сразу (файл не тот, строки читать бессмысленно), ошибки строк копятся, и хорошие строки всё равно возвращаются. Цепочка всегда одна: RowError -> обёртка fmt.Errorf -> сентинел и, если был, strconv.NumError.

Как это собирается ​

Обработчик загрузки получает файл, режет его на строки и вызывает Import. Дальше решение принимает он, а не импорт: errors.Is(err, ErrBadHeader) - отказать с "это не прайс", ошибки строк - сохранить хорошие строки и показать Report(err) списком, errors.Is(err, ErrDuplicateSKU) - добавить подсказку. Тексты ошибок нигде не сравниваются, поэтому их можно переписывать свободно.

Сценарий целиком с выводом: пример на Go.

Как это в других языках ​

PHP. Ошибки - исключения, и вместо сентинелов - иерархия классов: final class BadPrice extends ImportError. Контекст добавляют новым исключением с previous: new RowFailed("line 3", previous: $e), а цепочку читают через getPrevious(). Аналога errors.Join нет: собирают массив ошибок руками или бросают одно исключение со списком внутри.

Python. Тоже исключения: raise RowError(line=3) from err сохраняет причину в __cause__. С 3.11 есть прямой аналог errors.Join - ExceptionGroup, а except* BadPrice ловит нужные ошибки из группы, остальные летят дальше.

TypeScript. new Error("line 3", { cause: err }) хранит причину, AggregateError складывает несколько ошибок. Но catch (e) даёт unknown, и проверять тип приходится руками через instanceof. Поэтому для ожидаемых отказов вроде плохой строки часто берут Result-тип, как в теме о размеченных объединениях.

Запуск: make test T=languages/go/01-errors-and-wrapping

Тесты проверяют таблицу цен (хорошие и плохие значения сверяются по сентинелу), то, что RowError достаётся через errors.As вместе с номером строки и strconv.NumError, что Import собирает все ошибки строк в одну и что ошибка заголовка не похожа на ошибку строки.

Плюсы и минусы ​

  • ✅ Видно, какие вызовы могут упасть: ошибка в сигнатуре, а не в документации.
  • ✅ Проверки по смыслу (errors.Is, errors.As) переживают любую правку текста и любое число обёрток.
  • ✅ errors.Join превращает "упали на первой" в "вот все проблемы файла" без своего типа-коллекции.
  • ❌ if err != nil после каждого вызова, и контекст в обёртку надо дописывать руками.
  • ❌ Сентинел - экспортируемая переменная пакета, часть публичного API: переименовать её - сломать клиентов.
  • ❌ Из errors.Join ошибки достаются через приведение к interface{ Unwrap() []error }, удобного метода в stdlib нет.

Когда не применять ​

  • Ошибка, которую вызывающий код не будет различать: хватит fmt.Errorf("...: %w", err) без своего сентинела.
  • Свой тип ошибки без полей: если в нём только текст, это сентинел, а не тип.
  • Ошибки, после которых продолжать нельзя (битый конфиг при старте): собирать их в Join незачем, вернуть первую.
  • Программная ошибка (nil-указатель, выход за массив): это panic, а не значение для обработки.

Связанные темы ​

© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS