Go-фича: Безопасная проверка ошибок

Часть серии Принято! В ней простыми словами объясняются новые фичи Go.

Проверка типа ошибки через errors.AsType — современная и безопасная альтернатива errors.As.

Версия 1.26 • Станд. библиотека • Важно

Что

Новая функция errors.AsType — это generic-версия errors.As:

// go 1.13+
func As(err error, target any) bool
// go 1.26+
func AsType[E error](err error) (E, bool)

Она безопасная (type-safe), работает быстрее и проще в использовании:

// используем errors.As
var appErr AppError
if errors.As(err, &appErr) {
    fmt.Println("Got an AppError:", appErr)
}
// используем errors.AsType
if appErr, ok := errors.AsType[AppError](err); ok {
    fmt.Println("Got an AppError:", appErr)
}

errors.As не объявлена устаревшей (пока), но для нового кода рекомендуется использовать errors.AsType.

Зачем

Функция errors.As требует заранее объявить переменную ошибки нужного типа и передать указатель на нее:

var appErr AppError
if errors.As(err, &appErr) {
    fmt.Println("Got an AppError:", appErr)
}

Код получается довольно громоздким, особенно если нужно проверить несколько типов ошибок:

var connErr *net.OpError
var dnsErr *net.DNSError

if errors.As(err, &connErr) {
    fmt.Println("Network operation failed:", connErr.Op)
} else if errors.As(err, &dnsErr) {
    fmt.Println("DNS resolution failed:", dnsErr.Name)
} else {
    fmt.Println("Unknown error")
}

С помощью generic-функции errors.AsType можно указать тип ошибки прямо при вызове. Это делает код короче и позволяет держать переменные ошибок внутри блоков if:

if connErr, ok := errors.AsType[*net.OpError](err); ok {
    fmt.Println("Network operation failed:", connErr.Op)
} else if dnsErr, ok := errors.AsType[*net.DNSError](err); ok {
    fmt.Println("DNS resolution failed:", dnsErr.Name)
} else {
    fmt.Println("Unknown error")
}

Кроме того, As использует рефлексию и может вызвать панику, если использовать ее неправильно (например, если передать не указатель или тип, который не реализует error). Обычно статические анализаторы находят такие ошибки, но у AsType есть несколько преимуществ:

  • Не использует рефлексию.
  • Нет паники во время выполнения.
  • Меньше аллокаций памяти.
  • Проверка типов на этапе компиляции.
  • Работает быстрее.

Наконец, AsType умеет все то же, что и As, так что ее можно спокойно использовать вместо As в новом коде.

Подробности

Добавить в пакет errors функцию AsType:

// AsType finds the first error in err's tree that matches the type E,
// and if one is found, returns that error value and true. Otherwise, it
// returns the zero value of E and false.
//
// The tree consists of err itself, followed by the errors obtained by
// repeatedly calling its Unwrap() error or Unwrap() []error method.
// When err wraps multiple errors, AsType examines err followed by a
// depth-first traversal of its children.
//
// An error err matches the type E if the type assertion err.(E) holds,
// or if the error has a method As(any) bool such that err.As(target)
// returns true when target is a non-nil *E. In the latter case, the As
// method is responsible for setting target.
func AsType[E error](err error) (E, bool)

Рекомендовать использовать AsType вместо As:

// As finds the first error in err's tree that matches target, and if one
// is found, sets target to that error value and returns true. Otherwise,
// it returns false.
// ...
// For most uses, prefer [AsType]. As is equivalent to [AsType] but sets its
// target argument rather than returning the matching error and doesn't require
// its target argument to implement error.
// ...
func As(err error, target any) bool

Пример

Открыть файл и проверить, связана ли ошибка с путем к файлу:

// go 1.25
var pathError *fs.PathError
if _, err := os.Open("non-existing"); err != nil {
    if errors.As(err, &pathError) {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}
Failed at path: non-existing
// go 1.26
if _, err := os.Open("non-existing"); err != nil {
    if pathError, ok := errors.AsType[*fs.PathError](err); ok {
        fmt.Println("Failed at path:", pathError.Path)
    } else {
        fmt.Println(err)
    }
}
Failed at path: non-existing

Ссылки

𝗣 51945 • 𝗖𝗟 707235

★ Подписывайтесь на канал и проходите курсы.