Свой тип ошибки и errors.As
В прошлом уроке маркер ErrNotFound сообщал одно: «не найдено». Но вызывающему коду часто нужны подробности: какого пользователя не нашли, с каким кодом ответил сервер. Маркер — это штамп «не найдено» на накладной. А подробности вписывают в бланк с графами: «номер заказа», «причина».
Такой бланк в Go — свой тип ошибки. Это обычная структура с полями и методом Error() string, и поэтому она реализует интерфейс error:
package main
import "fmt"
type NotFoundError struct{ ID int }
func (e *NotFoundError) Error() string {
return fmt.Sprintf("user %d not found", e.ID)
}
func main() {
var err error = &NotFoundError{ID: 7}
fmt.Println(err) // user 7 not found
}Давайте разберём пример по строкам:
type NotFoundError struct{ ID int }объявляет бланк с одной графой — номером пользователя.- Метод
Errorс получателем-указателем собирает текст ошибки из поляID. var err error = &NotFoundError{ID: 7}кладёт в переменную типаerrorадрес структуры. Нужен именно адрес: методErrorесть у*NotFoundError, а у самой структуры его нет.fmt.Println(err)печатает текст из методаError:user 7 not found.
Имя такого типа по договорённости заканчивается на Error, а маркера — начинается с Err.
Функция errors.As
Пока ошибка лежит в переменной как есть, номер можно достать утверждением типа. Но функции оборачивают ошибки через %w, и снаружи оказывается конверт, а не бланк. Функция errors.As находит бланк в цепочке:
err := fmt.Errorf("load: %w", &NotFoundError{ID: 7})
var nf *NotFoundError
if errors.As(err, &nf) {
fmt.Println("not found:", nf.ID) // not found: 7
}Разберём пример по строкам:
err— конверт с пометкойload:, а внутри него бланкNotFoundError.var nf *NotFoundErrorзаводит пустую переменную того типа, который мы ищем.errors.As(err, &nf)открывает конверты по очереди. Найдя ошибку типа*NotFoundError, функция кладёт её вnfи возвращаетtrue.- Внутри
ifуnfуже есть поля, и программа печатаетnot found: 7.
Второй аргумент — адрес переменной, &nf: так функция может положить в неё находку.
errors.Is или errors.As
Этот вопрос часто задают на собеседовании: чем отличаются две функции? Разберём каждую.
errors.Is ищет значение
errors.Is(err, target) ищет в цепочке то самое значение target. Это подходит для маркера: ErrNotFound в программе один. Ошибку своего типа errors.Is обычно не находит:
err := fmt.Errorf("load: %w", &NotFoundError{ID: 7})
fmt.Println(errors.Is(err, &NotFoundError{ID: 7})) // falseКаждый &NotFoundError{...} — адрес новой структуры. Это два дома с одинаковой обстановкой, но по разным адресам, и == для указателей даёт false.
errors.As ищет тип
errors.As(err, &target) ищет в цепочке любую ошибку нужного типа, какие бы значения ни лежали в её полях. Поэтому для своих типов ошибок выбирают её.
Здесь картинка с бланком заканчивается: в жизни бланк узнают по заголовку, а errors.As узнаёт ошибку только по типу. Два разных типа с одинаковыми полями для неё — разные бланки.
Частая ошибка
В errors.As передали саму переменную, а не её адрес:
err := fmt.Errorf("load: %w", &NotFoundError{ID: 7})
var nf *NotFoundError
fmt.Println(errors.As(err, nf)) // ✗panic: errors: target must be a non-nil pointerПрограмма компилируется, но при запуске останавливается с паникой: nf — пустой указатель, и класть находку некуда. Команда go vet замечает такую ошибку заранее. Передавайте адрес: errors.As(err, &nf).
Главное
- Свой тип ошибки — структура с полями и методом
Error() string; он несёт подробности, которых нет у маркера. errors.As(err, &target)ищет в цепочке ошибку нужного типа и кладёт её вtarget.errors.Isищет конкретное значение и подходит для маркеров, аerrors.Asищет тип и подходит для своих типов ошибок.- Второй аргумент
errors.As— адрес переменной:&nf, а неnf.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.