Errors.Is и errors.As в Go: как распознать ошибку после обёртки

Errors.Is и errors.As в Go: как распознать ошибку после обёртки
Содержание статьи

В Go ошибка может получить дополнительный контекст через fmt.Errorf с %w. После этого прямое сравнение или утверждение типа внешней ошибки часто уже не подходит. errors.Is проверяет совпадение с целевой ошибкой в цепочке, а errors.As находит ошибку подходящего типа и позволяет прочитать её поля.

В разборе Mentoring.digital используем учебную ошибку проверки поля. Пример работает на Go 1.13 и новее и не обращается к внешним сервисам.

Сохраните исходную причину

package main

import (
    "errors"
    "fmt"
)

var ErrRequired = errors.New("required value")

type FieldError struct {
    Field string
    Err   error
}

func (e *FieldError) Error() string {
    return fmt.Sprintf("field %s: %v", e.Field, e.Err)
}

func (e *FieldError) Unwrap() error {
    return e.Err
}

func validateName(name string) error {
    if name == "" {
        return &FieldError{Field: "name", Err: ErrRequired}
    }
    return nil
}

func main() {
    cause := validateName("")
    if cause == nil {
        return
    }
    err := fmt.Errorf("create profile: %w", cause)
    fmt.Println(errors.Is(err, ErrRequired))

    var fieldErr *FieldError
    if errors.As(err, &fieldErr) {
        fmt.Println(fieldErr.Field)
    }
}

Сохраните код в main.go и запустите go run main.go. Ожидаются строки true и name. Первая проверка находит ErrRequired за двумя обёртками. Вторая находит FieldError и предоставляет имя поля. В нашей функции пустота означает только name == "": строка из пробелов этим правилом не запрещена.

Is проверяет причину, а не текст

ErrRequired создаётся один раз как отдельное значение. Новый вызов errors.New с тем же сообщением создаст другую ошибку. Поэтому не пересоздавайте целевую ошибку прямо внутри errors.Is и не сравнивайте строки Error() ради бизнес-решения.

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

As извлекает подходящий тип

Переменная fieldErr имеет тип *FieldError. В errors.As передаётся адрес переменной: &fieldErr. Функция при успехе записывает туда найденный указатель. Это не создание нового FieldError и не преобразование строки в структуру.

Обычное err.(*FieldError) проверяет только внешний объект. В примере внешним объектом является результат fmt.Errorf, поэтому прямое утверждение типа не найдёт внутреннюю структуру. As проходит по раскрываемым ошибкам, учитывая правила сопоставления типов.

Проверьте сохранение цепочки

  • Замените %w на %v во внешнем fmt.Errorf. Текст останется похожим, но причина перестанет быть доступна через эту обёртку.
  • Уберите Unwrap у FieldError. As ещё сможет найти FieldError, но Is не дойдёт через него до ErrRequired.
  • Передайте непустое имя. Функция должна вернуть nil.
  • Создайте errors.New с тем же текстом и убедитесь, что это не исходный ErrRequired.

Оборачивание через %w делает внутреннюю причину доступной вызывающему коду. Это часть контракта API: подумайте, готовы ли вы сохранять такую причину при смене реализации. Не превращайте каждую ожидаемую ошибку ввода в panic.

Практику обработки ошибок можно включить в курс Go с наставником. Для ревью реального контракта покажите ментору по Go и место возникновения ошибки, и код, который принимает по ней решение.

Похожие статьи по данной теме