Библиотека структурированных ошибок: иммутабельные sentinel-ошибки, обёртка с sentinel, контрактные ошибки с локализацией и кодогенерация из YAML-каталога.
Паттерн sentinel error (ошибка-страж) — это универсальная архитектурная концепция, при которой заранее определённые константы используются для обозначения конкретных бизнес-ошибок или состояний (например, «Пользователь не найден» или «Недостаточно средств»).
Вместо парсинга текста ошибки, принимающая сторона (на сервере или клиенте) просто сравнивает полученный код/экземпляр ошибки с известным списком (sentinel-значениями) и ветвит логику. Это делает обработку отказов предсказуемой и легко тестируемой на любом языке программирования. В Go для этого используется errors.Is, в Dart — сравнение строковых кодов или константных классов.
go get github.com/AleksandrGurkin/xerrorБиблиотека обеспечивает базовые строительные блоки для трёхтиповой модели ошибок: доменные, контрактные и коммуникационные.
| Компонент | Описание |
|---|---|
Const |
Иммутабельные константные ошибки, объявляемые на уровне пакета |
WrapWithSentinel |
Создаёт обёртку, связывающую ошибку зависимости с доменным sentinel. errors.Is работает для обоих |
WithDetails / DetailsFromError |
Прикрепляет контекстные детали к ошибке; детали путешествуют по цепочке и автоматически мержатся в ContractError |
CommunicationError |
Обёртка с хелперами WrapTimeout / WrapConnection и fluent API ( WithReason, WithRetryable) для обработки инфраструктурных сбоев |
ContractError |
Контрактная ошибка: код, HTTP/gRPC статус, Localize() для перевода сообщений на разные языки, WithDetails() с фильтрацией по публичности, WithDetailsFrom() для извлечения деталей из цепочки |
httperr |
Пакет для удобной записи и парсинга ContractError в HTTP JSON ответы ( WriteError, ParseResponse, LocalizationMiddleware ) |
grpcerr |
Пакет для автоматической трансформации ContractError в status.Status с errdetails.ErrorInfo (включая Server Interceptor) |
catalog |
YAML-каталог + парсер + валидация (формат кода, домен, HTTP-статус, метаданные) |
xerrorgen |
CLI-генератор Go и Dart кода из YAML-каталога |
const ErrNotFound = xerror.Const("NOT_FOUND")
func doSomething() error {
return ErrNotFound
}WrapWithSentinel создаёт обёртку, связывающую оригинальную ошибку с доменным sentinel. Типичное применение — трансформация контрактной ошибки зависимости в доменный sentinel.
// Доменные sentinel-ы определяются как константы:
const (
ErrUserNotFound = xerror.Const("USER_NOT_FOUND")
ErrNotEnoughMoney = xerror.Const("NOT_ENOUGH_MONEY")
)
// Инфраструктурный слой: контрактная ошибка зависимости → доменный sentinel
func (r *UserRepo) FindByID(ctx context.Context, id string) (*User, error) {
user, err := r.db.Get(ctx, id)
if errors.Is(err, sql.ErrNoRows) {
return nil, xerror.WrapWithSentinel(err, ErrUserNotFound)
}
return user, err
}Результат WrapWithSentinel — скрытый тип-обёртка (реализует error), который поддерживает:
Is(target)— сравнивает с sentinel:errors.Is(err, ErrUserNotFound)→trueUnwrap()— возвращает оригинал:errors.Is(err, sql.ErrNoRows)→trueError()—"USER_NOT_FOUND: sql: no rows in result set"
// Обе проверки работают:
errors.Is(err, ErrUserNotFound) // true — через кастомный Is()
errors.Is(err, sql.ErrNoRows) // true — через Unwrap()-цепочкуПозволяет обернуть неожиданные или инфраструктурные ошибки (например, таймауты) с добавлением контекста, причины сбоя (Reason) и флагом возможности повтора (Retryable). Поддерживается fluent API и готовые хелперы.
// Сбой связи с базой данных (хелпер сам поставит ReasonTimeout и Retryable = true)
err := doNetworkRequest()
if errors.Is(err, context.DeadlineExceeded) {
return xerror.WrapTimeout(err, "failed to query %s", "users table")
}
// Fluent API для нестандартных случаев (Внимание: используйте if err != nil, чтобы избежать typed nil)
if err != nil {
return xerror.WrapCommunication(err, "custom error").
WithReason(xerror.ReasonProtocol).
WithRetryable(false)
}
// Позже, на верхнем уровне:
type retryer interface{ Retryable() bool }
if r, ok := err.(retryer); ok && r.Retryable() {
// Начать exponential backoff...
}Ошибки, пересекающие границу сервиса. Генерируются из YAML-каталога через xerrorgen:
// Сгенерировано xerrorgen:
var AuthUserNotFound = xerror.ContractError{
Code: "AUTH_USER_NOT_FOUND",
HTTPStatus: 404,
GRPCCode: "NOT_FOUND",
Public: true,
Message: "User not found",
Messages: map[string]string{"en": "User not found", "ru": "Пользователь не найден"},
}Локализация — Localize выбирает сообщение по языку, по умолчанию "en":
resp := apperrors.AuthUserNotFound.Localize("ru")
// resp.Message == "Пользователь не найден"Детали — WithDetails добавляет контекст. Для публичных ошибок фильтрует по whitelist из YAML metadata:
resp := apperrors.BillingInsufficientFunds.
Localize("ru").
WithDetails(map[string]any{
"balance": 50.0, // ✓ в AllowedDetails → пропустит
"user_id": "abc", // ✗ не в AllowedDetails → отфильтрует
})Детали из цепочки ошибок — xerror.WithDetails прикрепляет контекст к ошибке на уровне домена. Детали путешествуют по цепочке и автоматически подхватываются WriteError / gRPC interceptor:
// В доменном сервисе — прикрепляем детали там, где есть контекст:
return xerror.WithDetails(
domain.ErrInsufficientFunds,
map[string]any{"balance": balance},
)
// В handler-е — WithDetailsFrom извлекает детали из цепочки и мержит в ContractError:
if errors.Is(err, domain.ErrInsufficientFunds) {
httperr.WriteError(ctx, w, apperrors.BillingInsufficientFunds.WithDetailsFrom(err))
}Для удобной передачи контрактных ошибок по сети библиотека предоставляет готовые адаптеры:
HTTP REST (xerror/httperr)
import "github.com/AleksandrGurkin/xerror/httperr"
// На сервере: подключаем LocalizationMiddleware (аналог grpcerr.UnaryServerInterceptor)
mux := http.NewServeMux()
mux.Handle("/orders", orderHandler)
server := http.Server{Handler: httperr.LocalizationMiddleware(mux)}
// В Handler-е: WriteError принимает ctx, а не *http.Request
func myHandler(w http.ResponseWriter, r *http.Request) {
err := doSomething()
httperr.WriteError(r.Context(), w, err) // Локализация через middleware автоматически
}
// На клиенте (HTTP Client):
resp, _ := httpClient.Do(req)
contractErr, parseErr := httperr.ParseResponse(resp)
if contractErr != nil {
fmt.Println(contractErr.Message) // Распакованный JSON -> ContractError
}gRPC (xerror/grpcerr)
Использует механизм Rich Errors, прикрепляя оригинальный код, статус transient и детали в errdetails.ErrorInfo.
import "github.com/AleksandrGurkin/xerror/grpcerr"
// На сервере достаточно подключить интерсептор:
server := grpc.NewServer(
grpc.UnaryInterceptor(grpcerr.UnaryServerInterceptor()),
)
// На клиенте:
resp, err := grpcClient.Charge(ctx, req)
if err != nil {
contractErr, ok := grpcerr.FromError(err)
if ok {
fmt.Println(contractErr.Message) // Распакованный статус -> ContractError
}
}Warning
Type Loss в gRPC Details
При передаче ContractError.Details через gRPC все значения сериализуются в строки (map[string]string), так как это ограничение формата gRPC Metadata. Если вы отправляли числа (123) или булевые значения (true), на клиенте в Details они будут десериализованы как строки ("123", "true"). Будьте внимательны при написании клиентской логики.
YAML-файл — единый источник правды для контрактных ошибок:
errors:
AUTH_USER_NOT_FOUND:
domain: auth
grpc_code: NOT_FOUND
http_status: 404
transient: false
public: true
messages:
ru: "Пользователь не найден"
en: "User not found"
description: "Returned when user ID does not exist"
metadata:
- name: retry_after
type: intПарсинг и валидация:
import "github.com/AleksandrGurkin/xerror/catalog"
c, err := catalog.ParseFile("errors/catalog.yaml")Валидация проверяет: формат кода (UPPER_SNAKE_CASE), совпадение домена с префиксом, HTTP-статус 400–599, наличие сообщений, типы метаданных.
Note
metadata → AllowedDetails: Поля из секции metadata в YAML-каталоге превращаются в AllowedDetails в сгенерированном Go-коде. Для публичных ошибок (public: true) это белый список — при вызове WithDetails() только ключи из этого списка попадут в ответ клиенту. Для непубличных ошибок все детали передаются без фильтрации.
CLI-инструмент для генерации Go и Dart кода из YAML-каталога:
# Go: pre-built ContractError instances
go run ./cmd/xerrorgen -lang=go -input=catalog.yaml -output=errors_gen.go -package=apperrors
# Dart: ErrorCodes, transientErrors, publicErrors, grpcCodes, fallbackMessages
go run ./cmd/xerrorgen -lang=dart -input=catalog.yaml -output=errors_gen.dartВ репозитории доступны полноценные примеры микросервисного взаимодействия с перехватом, пробросом и трансформацией ошибок между сервисами разного уровня:
- Классификация ошибок — трёхтиповая модель (контрактные, доменные, коммуникационные), трансформация между уровнями, каталогизация
См. файл LICENSE.