Программирование GoОбработка ошибокGo-разработчик серверных приложений

Представьте обработчик, который ищет конкретный тип ошибки. Какой контракт аргумента target нарушен в этом ...

Представьте обработчик, который ищет конкретный тип ошибки. Какой контракт аргумента target нарушен в этом фрагменте и какое последствие возникнет?

package main

import (
	"errors"
	"fmt"
)

type quotaError struct{}
func (*quotaError) Error() string { return "quota exceeded" }

func main() {
	var target *quotaError
	fmt.Println(errors.As(&quotaError{}, target))
}
Проходите собеседования с ИИ помощником Hintsage

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

Вызов завершится паникой: target должен быть ненулевым указателем на переменную, в которую errors.As запишет найденную ошибку. Здесь передан нулевой указатель (*quotaError)(nil), поэтому нарушен контракт аргумента target.

Правильный вариант — передать адрес переменной: errors.As(err, &target). Тогда errors.As сможет записать в target найденный объект ошибки.

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

До появления стандартных механизмов wrapping разработчики часто использовали прямые приведения типов и сравнение ошибок. Обёртка скрывала исходное значение, поэтому такие проверки становились хрупкими.

Механизм errors.As предназначен для поиска ошибки по типу в цепочке обёрток. Чтобы он мог вернуть найденное значение вызывающему коду, ему нужен указатель на переменную подходящего типа.

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

В примере переменная target объявлена правильно, но она содержит nil. Передача target в errors.As не даёт функции места, куда можно записать найденную ошибку: это нулевой указатель, а не адрес переменной.

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

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

Контракт errors.As требует, чтобы target был ненулевым указателем на тип, реализующий error, либо на интерфейсный тип. В корректном варианте переменная хранит указатель на найденную ошибку, а в функцию передаётся её адрес:

package main import ( "errors" "fmt" ) type quotaError struct{} func (*quotaError) Error() string { return "quota exceeded" } func main() { var target *quotaError ok := errors.As(&quotaError{}, &target) fmt.Println(ok, target != nil) }

Здесь &target имеет тип **quotaError. Это не означает, что errors.As ищет ошибку типа **quotaError: функция использует элемент указателя как целевой тип, то есть ищет *quotaError, а затем записывает найденное значение в переменную target.

Передача &target обязательна не только для указателей на конкретные типы. Аналогично передают адрес интерфейсной переменной: var temporary interface{ Temporary() bool }; errors.As(err, &temporary).

Если нужен только факт наличия ошибки определённого типа, результат bool можно проверить без дальнейшего использования переменной. Но даже в этом случае target должен быть корректным ненулевым указателем.

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

В HTTP-сервисе обработчик должен отличать ошибку превышения квоты от остальных ошибок. Один вариант — проверять текст сообщения. Он прост, но ломается при изменении формулировки и не даёт безопасного доступа к дополнительным данным ошибки.

Второй вариант — использовать прямое приведение типа. Оно работает для непосредственной ошибки, но перестаёт работать после добавления обёртки через %w.

Выбранное решение — объявить var target *quotaError и передавать &target в errors.As. Этот подход сохраняет работу через цепочку обёрток и позволяет после успешного поиска получить структурированные данные ошибки. Результат — устойчивый к изменению текста маршрутизационный код без риска паники из-за неверного аргумента.

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

  1. Можно ли передать в errors.As указатель на интерфейс?

    Да. Например, var temporary interface{ Temporary() bool }; errors.As(err, &temporary) корректен. errors.As найдёт ошибку, реализующую этот интерфейс, и запишет её в temporary. Передавать нужно именно &temporary, а не temporary, потому что функция должна изменить значение переменной.

  2. Что произойдёт, если передать указатель на тип, который не реализует error?

    errors.As завершится паникой из-за нарушения контракта целевого типа. Например, var s string; errors.As(err, &s) некорректен: элемент указателя — string, а не тип ошибки и не интерфейс. Это проверяется во время выполнения, поскольку сигнатура функции принимает any.

  3. Чем отличается нулевой указатель внутри интерфейса от корректного &target?

    Переменная target типа *quotaError может быть nil, но выражение &target всегда является ненулевым указателем на саму переменную. errors.As получает адрес, куда можно записать результат, и отдельно уже присваивает туда найденный нулевой или ненулевой объект согласно цепочке ошибок. Поэтому errors.As(err, target) и errors.As(err, &target) имеют принципиально разный контракт.