Три вида ошибок
В коде на Go встречаются три формы ошибок, от простой к самой богатой:
- Разовая ошибка:
errors.New("...")илиfmt.Errorf("..."), созданная на месте. Вызывающий код может только прочитать сообщение. - Ошибка-маркер (sentinel): переменная уровня пакета вроде
io.EOF. Вызывающий код может проверить её черезerrors.Is. - Тип ошибки: структура с методом
Error()и полями. Вызывающий код может извлечь её черезerrors.Asи прочитать поля.
Выбирайте самую простую форму, которая позволяет вызывающему коду сделать то, что ему нужно.
Ошибки-маркеры
Маркер это значение ошибки, объявленное один раз и сравниваемое по идентичности. По соглашению имена начинаются с Err.
Вывод:
mug bought
cap is out of stock, notify me later
buy "hat": not found
Два вызова errors.New с одинаковым текстом дают разные ошибки: errors.New("x") == errors.New("x") равно false. Поэтому маркер должен быть одной общей переменной.
Маркеры становятся частью API вашего пакета. Как только вызывающий код начал проверять ErrNotFound, перестать его возвращать без поломки нельзя. Экспортируйте только те, по которым вызывающему коду действительно нужно ветвиться.
Собственные типы ошибок
Когда вызывающему коду нужны подробности (какое поле, какой код статуса, какая задержка перед повтором), определите тип.
Вывод:
register: age: must be between 0 and 150
bad field: age
У метода получатель-указатель, а функция возвращает &ValidationError{...}, поэтому цель для errors.As это *ValidationError, и вы передаёте её адрес (&ve, то есть **ValidationError). Ошибиться с этим уровнем косвенности это классическая ошибка с errors.As: с получателем-значением вы бы возвращали ValidationError{...} и объявляли var ve ValidationError. Самый частый промах, передачу ve вместо &ve, ловит go vet: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type.
Оборачивание через %w
fmt.Errorf с %w возвращает ошибку, которая помнит ту, что обёрнута. Каждый слой добавляет контекст и сохраняет цепочку.
Вывод:
start: connect db: timeout
*fmt.wrapError: start: connect db: timeout
*fmt.wrapError: connect db: timeout
*errors.errorString: timeout
true
false
С %v сообщение то же, но цепочка обрывается, поэтому errors.Is возвращает false. Выбирайте %w, когда вызывающий код должен видеть причину, и %v, когда причина это деталь реализации, от которой он не должен зависеть.
Начиная с Go 1.20 один вызов может обернуть несколько ошибок: fmt.Errorf("%w; %w", err1, err2). Тогда errors.Is совпадает с любой из них.
errors.Join: несколько ошибок сразу
Проверка данных и очистка ресурсов часто дают больше одной ошибки. errors.Join (Go 1.20) собирает их вместе.
Вывод:
name: required
email: required
age: negative
---
true
true
errors.Join возвращает nil, когда все аргументы nil, поэтому функции выше не нужен особый случай для «ошибок нет».
Методы Unwrap и Is
errors.Is и errors.As находят обёрнутые ошибки, вызывая метод Unwrap. Собственный тип, который хранит причину, должен её отдавать:
Тип, который оборачивает несколько ошибок, вместо этого реализует Unwrap() []error.
Тип может также определить Is(target error) bool, чтобы самому решать, что считать равенством, например совпадение любого *HTTPError с тем же кодом статуса. Это нужно редко; обычно достаточно обернуть маркер и возвращать его из Unwrap.
Ловушка типизированного nil
Никогда не объявляйте переменную ошибки со своим конкретным типом, чтобы потом вернуть её как error:
func check() error {
var err *ValidationError // nil pointer
// ... no problem found
return err // non-nil error! Its type is *ValidationError
}
if err != nil в вызывающем коде даст true, потому что интерфейс с типизированным nil-указателем не равен nil. При успехе возвращайте литерал nil, а локальные переменные ошибок объявляйте с типом error. Почему так, объясняет страница об интерфейсах.
Что выбрать
| Вызывающему коду нужно | Что предоставить |
|---|---|
| только залогировать или показать сбой | fmt.Errorf("...: %w", err) |
| ветвиться по одному конкретному условию | маркер var ErrX = errors.New(...) |
| прочитать подробности сбоя | тип ошибки с полями |
| увидеть несколько независимых сбоев | errors.Join |
Частые ошибки
- Сравнение обёрнутых ошибок через
==. Используйтеerrors.Is. - Передача в
errors.Asне указателя. Нужен указатель на переменную целевого типа. - «Маркер», созданный внутри функции.
return errors.New("not found")при каждом вызове создаёт новое значение; вызывающий код не сможет с ним сравнить. - Экспорт каждой ошибки. Каждый экспортированный маркер или тип это обещание в API.
- Сопоставление по
err.Error(). Строки предназначены для людей.
Часто задаваемые вопросы
Как создать собственную ошибку в Go?
Для фиксированного условия объявите маркер на уровне пакета: var ErrNotFound = errors.New("not found"). Для ошибки, которая несёт данные, определите тип с методом Error() string: type ValidationError struct { Field string } и func (e *ValidationError) Error() string { return e.Field + " is invalid" }.
Как обернуть ошибку в Go?
Используйте fmt.Errorf с глаголом %w: return fmt.Errorf("load user %d: %w", id, err). Сообщение новой ошибки включает старое, а errors.Unwrap, errors.Is и errors.As могут добраться до исходной. Начиная с Go 1.20 один вызов Errorf может обернуть несколько ошибок несколькими глаголами %w.
Чем errors.Is отличается от errors.As?
errors.Is(err, target) отвечает на вопрос «есть ли это конкретное значение ошибки где-то в цепочке», для маркеров вроде io.EOF. errors.As(err, &target) отвечает на вопрос «есть ли в цепочке ошибка этого типа» и, если есть, сохраняет её в target, чтобы можно было прочитать поля.
Что делает errors.Join?
errors.Join(errs...) (Go 1.20) объединяет несколько ошибок в одну. Её сообщение это отдельные сообщения, разделённые переводами строк; nil-ошибки отбрасываются, а если все аргументы nil, возвращается nil. errors.Is и errors.As срабатывают, если совпадает любая из объединённых ошибок.