Обёртка %w и errors.Is
В прошлом уроке функция total возвращала ошибку наверх как есть. Часто к ней добавляют контекст: что именно делал код, когда всё сломалось. Представьте, что исходную накладную кладут в конверт и пишут на конверте свою пометку: «при загрузке пользователя 7». Пометка новая, а исходная накладная лежит внутри.
Ошибку с контекстом создают через fmt.Errorf и %w. Её текст содержит исходное сообщение, но сравнение == с исходной ошибкой даёт false:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func main() {
err := fmt.Errorf("load user 7: %w", ErrNotFound)
fmt.Println(err) // load user 7: not found
fmt.Println(err == ErrNotFound) // false
}Давайте разберём пример по строкам:
var ErrNotFound = errors.New("not found")объявляет исходную ошибку на уровне пакета.fmt.Errorf("load user 7: %w", ErrNotFound)кладёт её в конверт с пометкой.- Первый
Printlnпечатает весь текст: пометку с конверта и через двоеточие исходное сообщение. - Второй печатает
false:fmt.Errorfвернула новое значение ошибки — конверт, а неErrNotFound.
А вызывающему коду нужно узнать, какая ошибка случилась, а не только прочитать текст.
Ошибка-маркер
Переменную ErrNotFound в примере объявили на уровне пакета. Такую переменную называют ошибкой-маркером: это одно известное значение, по которому код узнаёт определённую неудачу. Её имя по договорённости начинается с Err. Функция возвращает маркер как есть:
func findUser(id int) (string, error) {
if id != 1 {
return "", ErrNotFound
}
return "ann", nil
}Маркер узнают именно по значению, а не по тексту. Другая ошибка из errors.New("not found") ему не равна, хотя текст у них одинаковый.
Цепочка ошибок
Обёртка через %w кладёт исходную ошибку внутрь новой. Обёртку можно обернуть ещё раз, и получится цепочка ошибок — конверт в конверте.
Функция errors.Is(err, target) проходит по цепочке от err вглубь и возвращает true, если встретит target. Порядок аргументов важен:
- первым идёт
err— проверяемая ошибка; - вторым идёт
target— маркер, который в ней ищут.
err := fmt.Errorf("load user 7: %w", ErrNotFound)
fmt.Println(errors.Is(err, ErrNotFound)) // true
again := fmt.Errorf("start: %w", err)
fmt.Println(again) // start: load user 7: not found
fmt.Println(errors.Is(again, ErrNotFound)) // trueДавайте разберём пример по строкам:
err— конверт с пометкойload user 7:и маркером внутри.errors.Is(err, ErrNotFound)вскрывает конверт, находит маркер и печатаетtrue.againкладётerrещё в один конверт с пометкойstart:.fmt.Println(again)печатает удлинившийся текст:start: load user 7: not found.errors.Is(again, ErrNotFound)вскрывает оба конверта по очереди и снова находит маркер.
Здесь картинка с конвертами заканчивается: errors.Is ищет не листок с похожим текстом, а тот самый маркер — то же значение.
Нажимайте кнопку: сначала она обернёт маркер в два конверта, потом errors.Is откроет их по одному. Следите, как растёт текст ошибки и на каком слое найдётся маркер.
err := ErrNotFound — маркер с текстом "not found". Нажмите кнопку, чтобы обернуть его.
Три способа проверить ошибку
Этот вопрос часто задают на собеседовании: чем errors.Is отличается от ==? Чтобы ответить, сравним три способа узнать маркер.
Сравнение текста
Запись err.Error() == "not found" сравнивает текст ошибки со строкой. Такая проверка сломается от первой же правки сообщения. Маркеры для того и нужны, чтобы текст можно было менять, ничего не сломав.
Сравнение ==
Оператор == сравнивает только внешнюю ошибку. Без обёртки он работает: findUser(7) возвращает то же самое значение ErrNotFound, и err == ErrNotFound даёт true. Но стоит обернуть маркер, и == даёт false, как в первом примере урока.
Функция errors.Is
errors.Is проверяет всю цепочку. Без обёртки она тоже работает: для самого ErrNotFound цепочка состоит из одной ошибки. Поэтому ошибку-маркер всегда проверяйте через errors.Is, а не через ==. Функция, которая сегодня возвращает маркер как есть, завтра может его обернуть.
Маркеры есть и в стандартной библиотеке. Ошибка strconv.Atoi("x") оборачивает strconv.ErrSyntax, поэтому errors.Is(err, strconv.ErrSyntax) для неё равно true, а err == strconv.ErrSyntax — false.
Рядом с errors.Is на собеседовании часто вспоминают errors.As. Она ищет в цепочке не конкретное значение, а ошибку нужного типа — например, своего типа с методом Error().
Частая ошибка
Контекст добавили через %v вместо %w:
err := fmt.Errorf("load user 7: %v", ErrNotFound) // ✗
fmt.Println(errors.Is(err, ErrNotFound)) // falseТекст ошибки тот же, load user 7: not found, но %v вставляет только строку. Это как переписать текст накладной на конверт, а саму накладную выбросить. Исходной ошибки внутри нет, и errors.Is её не находит. Чтобы цепочка сохранилась, оборачивайте через %w.
Главное
fmt.Errorfс%wкладёт исходную ошибку внутрь новой, и так получается цепочка.==сравнивает только внешнюю ошибку, аerrors.Is(err, target)ищетtargetпо всей цепочке.- Ошибку-маркер вроде
ErrNotFoundпроверяют черезerrors.Is, а не через==или текст. %vсохраняет только текст, иerrors.Isисходную ошибку уже не найдёт.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.