Go errors.As: как получить поля ошибки после обёртки
Сервис резервирования вернул отказ с кодом no_capacity и идентификатором ресурса. Клиент добавил к нему контекст, а обработчик стал сравнивать err.Error() со строкой. После изменения сообщения нужная ветка перестала срабатывать. Здесь вызывающему коду требуется не точный текст и не только факт ошибки, а структурированные поля отказа.
Сохранить поля причины
Для такого контракта подходит собственный тип, реализующий Error. Он хранит стабильный код и данные, которые разрешено использовать выше по стеку. Промежуточный слой оборачивает ошибку через fmt.Errorf с %w. errors.As проходит доступное дерево причин и присваивает первую подходящую ошибку целевой переменной. Добавление контекста больше не уничтожает возможность прочитать Code.
Цель передаётся указателем на переменную нужного типа. Если метод Error определён у *RemoteError, переменная имеет тип *RemoteError, а в As передаётся &remote. Получается указатель на указатель, потому что функция должна записать найденный указатель в переменную вызывающего кода. Это часто выглядит непривычно, но следует обычному правилу передачи места для результата.
В примере HTTP-код выбирается по полю Code. Текст внутреннего сообщения не выдаётся клиенту. Неизвестный тип ошибки получает 502, известный отказ вместимости — 409. Это решение принадлежит границе API: типизированные поля ещё не обязаны становиться публичными заголовками или полным JSON внешнего сервиса.
Получить тип после обёртки
package main
import (
"errors"
"fmt"
"net/http"
)
type RemoteError struct {
Code string
Resource string
}
func (e *RemoteError) Error() string {
return fmt.Sprintf("remote %s for %s", e.Code, e.Resource)
}
func statusFor(err error) int {
var remote *RemoteError
if errors.As(err, &remote) && remote.Code == "no_capacity" {
return http.StatusConflict
}
return http.StatusBadGateway
}
func main() {
cause := &RemoteError{Code: "no_capacity", Resource: "room-7"}
wrapped := fmt.Errorf("reserve meeting: %w", cause)
var found *RemoteError
if !errors.As(wrapped, &found) || found.Resource != "room-7" {
panic("typed cause lost")
}
if statusFor(wrapped) != 409 || statusFor(errors.New("offline")) != 502 {
panic("wrong public status")
}
fmt.Println(found.Code, found.Resource, statusFor(wrapped))
}
Код сохраняется как main.go и запускается go run main.go. Проверка читает Resource после обёртки, затем отдельно проверяет перевод в HTTP-статус. Это два разных свойства. Сохранить тип недостаточно, если обработчик всё равно выбирает статус по тексту. И наоборот, одна случайно правильная строка ответа не доказывает сохранение полей.
Выбрать публичный контракт ошибки
errors.Is полезен для узнавания известного значения или семантической причины. errors.As нужен, когда важно получить подходящий тип или интерфейс. Можно выбрать интерфейс с методом Retryable, если обработчику не следует зависеть от конкретного клиента. Но такой метод тоже становится контрактом: его смысл должен соответствовать безопасности самой операции, а не только доступности сети.
Решите, какие типы ошибок пакет намеренно раскрывает. Обёртка %w позволяет верхнему слою зависеть от причины, поэтому замена библиотеки может изменить наблюдаемое поведение. Если внутренние ошибки драйвера не должны быть частью API, переводите их в собственный тип на границе адаптера. Тогда бизнес-слой зависит от стабильного решения вашего приложения.
Не возвращайте типизированный nil-указатель как успешный error: интерфейс в таком случае может оказаться ненулевым. Это отдельная проблема, разобранная в статье про типизированный nil. В нашем контракте неудача всегда содержит настоящий RemoteError с проверенным кодом, а успех возвращает nil напрямую.
Добавьте тест на неизвестный код того же типа, два уровня обёртки и ошибку другого типа. Неизвестный код не должен случайно получить пользовательский статус известного отказа. Для публичного ответа выбирайте собственное короткое сообщение, а исходный ресурс и диагностический контекст оставляйте в разрешённом внутреннем журнале.
Продолжить практику
В курсе микросервисов на Go вы разберёте ответы зависимого сервиса, HTTP-контракты и перевод отказов в понятное поведение клиента. Откройте программу и продолжите с контрактного теста: он должен проверять, какой ответ получит пользователь после конкретного отказа другой программы.
Продолжить на практике
- Микросервисы на Go: HTTP, PostgreSQL, gRPC, Kafka и Docker ↗
От работающего Go API — к взаимодействию сервисов, которому можно доверять. Разберитесь, как проектировать контракты, сохранять согласованность данных и восстанавливать обработку после сбоев. HTTP, PostgreSQL, gRPC, Kafka и Docker — через код и наглядные разборы.
