Программирование GoОбработка ошибокРазработчик Go среднего уровня

В цепочке обёрток две ошибки одного типа. Какую ошибку получит errors.As? пример с кодом

В цепочке обёрток две ошибки одного типа. Какую ошибку получит errors.As?

package main

import (
	"errors"
	"fmt"
)

type tagged struct { name string; cause error }
func (e *tagged) Error() string { return e.name }
func (e *tagged) Unwrap() error { return e.cause }

func main() {
	inner := &tagged{name: "inner"}
	outer := &tagged{name: "outer", cause: inner}
	var got *tagged
	errors.As(outer, &got)
	fmt.Println(got.name)
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

errors.As получит внешнюю ошибку outer, потому что обходит цепочку начиная с самой переданной ошибки. Поиск останавливается на первом значении, которое присваивается целевому типу; результат программы — outer.

Исторический контекст

В Go ошибки традиционно возвращаются как значения интерфейса error, а дополнительный контекст добавляется обёртками. Начиная с Go 1.13 стандартная библиотека предоставила errors.Is, errors.As и механизм Unwrap, чтобы анализировать причины без разбора текстовых сообщений.

Такой подход решил проблему ручного обхода цепочек и хрупких проверок через строки. Вместо этого вызывающий код может искать категорию ошибки или конкретный тип по формальному контракту.

Постановка проблемы

В цепочке могут находиться несколько значений одного типа: например, внешняя ошибка содержит операционный контекст, а внутренняя — исходные данные. Если разработчик ожидает внутреннее значение, но использует errors.As без учёта порядка обхода, он получит другой объект.

Это особенно важно, когда тип ошибки содержит поля, определяющие действие: код ответа, имя ресурса или признак временности. Неверно выбранный экземпляр может привести к неправильному логированию, повтору операции или формированию ответа клиенту.

Подробное решение

errors.As(err, &target) проверяет саму err, затем последовательно обходит её причины через Unwrap() error. При первом значении, которое совместимо с типом переменной target, оно присваивается этой переменной, и поиск завершается.

В примере outer уже имеет тип *tagged, поэтому errors.As не доходит до inner. Метод Unwrap нужен только для продолжения поиска, если текущее значение не подошло.

var got *tagged if errors.As(err, &got) { fmt.Println(got.name) }

Второй аргумент должен быть указателем на переменную нужного типа: &got, а не got. Для интерфейсного поиска целевая переменная также должна быть указателем на интерфейс. Если подходящего значения нет, errors.As возвращает false и не сообщает найденный объект.

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

Ситуация из практики

Сервис оборачивает ошибку HTTP-запроса типом RequestError, а библиотека внутри также возвращает RequestError с исходным кодом отказа. Обработчик вызывает errors.As и получает внешнюю ошибку, в которой код описывает общий этап операции, а не первопричину.

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

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

В результате обработчик получает предсказуемую классификацию ошибки, а внутренние детали не определяют случайно поведение внешнего кода.

Что кандидаты часто упускают

1. Что произойдёт, если внешняя ошибка подходит целевому типу, но её Unwrap возвращает ошибку другого типа?

Будет возвращена внешняя ошибка, а внутренняя не будет проверяться. errors.As не ищет наиболее глубокое или наиболее специфичное совпадение — он использует первое совпадение в порядке обхода.

2. Имеет ли значение, является ли метод Unwrap указательным или значенческим?

Да. Метод должен быть доступен у фактического значения ошибки, переданного в интерфейсе error. Если Unwrap объявлен только у указателя, а в error помещено значение структуры, оно не будет распознано как обёртка, и обход причины не продолжится.

3. Как изменяется идея порядка поиска для ошибки с Unwrap() []error?

Для многопричинной ошибки обходится дерево причин: сначала проверяется текущая ошибка, затем её дочерние причины в порядке, определённом реализацией обхода. Поэтому при нескольких совпадениях также нельзя полагаться на получение «самой глубокой» причины; проектировать API нужно так, чтобы первое совпадение было приемлемым результатом.