В цепочке обёрток две ошибки одного типа. Какую ошибку получит 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)
}
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 нужен только для продолжения поиска, если текущее значение не подошло.
Второй аргумент должен быть указателем на переменную нужного типа: &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 нужно так, чтобы первое совпадение было приемлемым результатом.