Перейти к содержимому
Теория · Ошибки · урок 6 из 11

Обёртка %w и errors.Is

В прошлом уроке функция total возвращала ошибку наверх как есть. Часто к ней добавляют контекст: что именно делал код, когда всё сломалось. Представьте, что исходную накладную кладут в конверт и пишут на конверте свою пометку: «при загрузке пользователя 7». Пометка новая, а исходная накладная лежит внутри.

Ошибку с контекстом создают через fmt.Errorf и %w. Её текст содержит исходное сообщение, но сравнение == с исходной ошибкой даёт false:

GO
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
}

Давайте разберём пример по строкам:

  1. var ErrNotFound = errors.New("not found") объявляет исходную ошибку на уровне пакета.
  2. fmt.Errorf("load user 7: %w", ErrNotFound) кладёт её в конверт с пометкой.
  3. Первый Println печатает весь текст: пометку с конверта и через двоеточие исходное сообщение.
  4. Второй печатает false: fmt.Errorf вернула новое значение ошибки — конверт, а не ErrNotFound.

А вызывающему коду нужно узнать, какая ошибка случилась, а не только прочитать текст.

Ошибка-маркер

Переменную ErrNotFound в примере объявили на уровне пакета. Такую переменную называют ошибкой-маркером: это одно известное значение, по которому код узнаёт определённую неудачу. Её имя по договорённости начинается с Err. Функция возвращает маркер как есть:

GO
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 — маркер, который в ней ищут.
GO
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

Давайте разберём пример по строкам:

  1. err — конверт с пометкой load user 7: и маркером внутри.
  2. errors.Is(err, ErrNotFound) вскрывает конверт, находит маркер и печатает true.
  3. again кладёт err ещё в один конверт с пометкой start:.
  4. fmt.Println(again) печатает удлинившийся текст: start: load user 7: not found.
  5. errors.Is(again, ErrNotFound) вскрывает оба конверта по очереди и снова находит маркер.

Здесь картинка с конвертами заканчивается: errors.Is ищет не листок с похожим текстом, а тот самый маркер — то же значение.

Нажимайте кнопку: сначала она обернёт маркер в два конверта, потом errors.Is откроет их по одному. Следите, как растёт текст ошибки и на каком слое найдётся маркер.

ВживуюЦепочка ошибокслоёв: 0 · errors.Is → …
ErrNotFound
err.Error() → "not found"

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:

GO
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 и подпишись на канал — откроются все уроки.

Войти другим способом