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

Свой тип ошибки и errors.As

В прошлом уроке маркер ErrNotFound сообщал одно: «не найдено». Но вызывающему коду часто нужны подробности: какого пользователя не нашли, с каким кодом ответил сервер. Маркер — это штамп «не найдено» на накладной. А подробности вписывают в бланк с графами: «номер заказа», «причина».

Такой бланк в Go — свой тип ошибки. Это обычная структура с полями и методом Error() string, и поэтому она реализует интерфейс error:

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

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

  1. type NotFoundError struct{ ID int } объявляет бланк с одной графой — номером пользователя.
  2. Метод Error с получателем-указателем собирает текст ошибки из поля ID.
  3. var err error = &NotFoundError{ID: 7} кладёт в переменную типа error адрес структуры. Нужен именно адрес: метод Error есть у *NotFoundError, а у самой структуры его нет.
  4. fmt.Println(err) печатает текст из метода Error: user 7 not found.

Имя такого типа по договорённости заканчивается на Error, а маркера — начинается с Err.

Функция errors.As

Пока ошибка лежит в переменной как есть, номер можно достать утверждением типа. Но функции оборачивают ошибки через %w, и снаружи оказывается конверт, а не бланк. Функция errors.As находит бланк в цепочке:

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

Разберём пример по строкам:

  1. err — конверт с пометкой load:, а внутри него бланк NotFoundError.
  2. var nf *NotFoundError заводит пустую переменную того типа, который мы ищем.
  3. errors.As(err, &nf) открывает конверты по очереди. Найдя ошибку типа *NotFoundError, функция кладёт её в nf и возвращает true.
  4. Внутри if у nf уже есть поля, и программа печатает not found: 7.

Второй аргумент — адрес переменной, &nf: так функция может положить в неё находку.

errors.Is или errors.As

Этот вопрос часто задают на собеседовании: чем отличаются две функции? Разберём каждую.

errors.Is ищет значение

errors.Is(err, target) ищет в цепочке то самое значение target. Это подходит для маркера: ErrNotFound в программе один. Ошибку своего типа errors.Is обычно не находит:

GO
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 передали саму переменную, а не её адрес:

GO
err := fmt.Errorf("load: %w", &NotFoundError{ID: 7})
var nf *NotFoundError
fmt.Println(errors.As(err, nf)) // ✗
TEXT
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 и подпишись на канал — откроются все уроки.

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