Каждый Go-разработчик знаком с этим паттерном — создание обёрток для ошибок с дублированием метаданных:
func (*SomeObject) SomeMethod(val any) error {
if err := otherMethod(val); err != nil {
return fmt.Errorf("otherMethod %w with val %v", err, val)
}
return nil
}
Проблемы такого подхода:
- Дублирование названий методов в сообщениях об ошибках
- Ручное добавление метаданных (аргументы, переменные) в каждое место
- Сложность отслеживания места возникновения ошибки при нескольких точках выхода
- Засорение кода — повторяющийся boilerplate
- Отсутствие структуры — все метаданные упакованы в одной строке
Что если объединить мощь структурированного логирования (slog
) с автоматическим сбором локального стека вызовов. Результат — чистый код и информативные логи.
Решение: структурированные ошибки + автоматический стек
Было:
func (*SomeObject) SomeMethod(val any) error {
if err := otherMethod(val); err != nil {
slog.Error(err, "val", val) // дублирование
return fmt.Errorf("someMethod %w with val %v", err, val)
}
return nil
}
Стало:
func (*SomeObject) SomeMethod(val any) (err error) {
defer logger.DebugOrError("debug description", &err)
if err := otherMethod(val); err != nil {
return log.WrapError(err, "val", val)
}
return nil
}
Ключевые возможности
1. Автоматический стек вызовов
Больше не нужно вручную указывать названия методов — стек собирается автоматически:
level=ERROR msg="other error" val=nil
stack[0]="lib.go:26" stack[1]="lib.go:22"
2. Умная идентификация точек ошибок
При нескольких возможных местах возникновения ошибки:
func (*SomeObject) SomeMethod() (err error) {
defer logger.DebugOrError("debug description", &err)
if err := otherMethod1(); err != nil {
return log.WrapError(err) // line 25
}
if err := otherMethod2(); err != nil {
return log.WrapError(err) // line 29
}
return nil
}
Получаем точное указание места в логах:
level=ERROR msg="other error 1"
stack[0]="lib.go:25" stack[1]="lib.go:22"
level=ERROR msg="other error 2"
stack[0]="lib.go:29" stack[1]="lib.go:22"
3. Создание новых структурированных ошибок
err = log.NewError("text", "key1", "value1", "key2", "value2")
4. Умное обёртывание
При многократном обёртывании стек сохраняется от первого вызова:
err = log.WrapError(err, "arg1", "val1") // стек записывается здесь
err = log.WrapError(err, "arg2", "val2") // стек сохраняется
5. Защита от забытых обёрток
Если забыли обернуть ошибку, DebugOrError
всё равно добавит стек:
func (*SomeObject) SomeMethod() (err error) {
// стек всё равно добавится, но только на этот вызов
defer logger.DebugOrError("debug description", &err)
return errors.New("unwrapped error")
}
6. Накопление метаданных
Аргументы из всех уровней обёртывания объединяются:
func (*SomeObject) SomeMethod() (err error) {
defer logger.DebugOrError("debug description", &err, "arg3", "val3")
err = log.NewError("my error", "arg1", "val1")
// ...
err = log.WrapError(err, "arg2", "val2")
return err
}
Результат:
level=ERROR msg="my error" arg1=val1 arg2=val2 arg3=val3
stack[0]="lib.go:25" stack[1]="lib.go:22"
Архитектура решения
Структурированные ошибки
type CustomError struct {
error
Args []any // структурированные аргументы
Stack []*CallInfo // локальный стек вызовов
}
Умный сбор стека
- Автоматически определяет границы проекта по
go.mod
- Показывает только релевантный код (исключает stdlib и зависимости)
- Предоставляет читаемые пути относительно корня проекта
- Кеширует метаинформацию для производительности
API для логирования
// Логирование с автоматическим переключением уровня при ошибке
logger.DebugOrError("operation completed", &err, "user_id", 123)
logger.InfoOrError("data processed", &err, "records", count)
logger.WarnOrError("cache miss", &err, "key", cacheKey)
Преимущества
✅ Чистота кода — убрали boilerplate
✅ Автоматические метаданные — стек и аргументы собираются сами
✅ IDE-friendly — клик по ссылке ведёт прямо к проблемному коду
✅ Производительность — кеширование метаданных проекта
✅ Безопасность — защита от забытых обёрток
✅ Масштабируемость — работает с любой структурой проекта
Заключение
Структурированные ошибки с локальным стеком вызовов — это естественная эволюция обработки ошибок в Go. Они сохраняют философию явности языка, но избавляют от рутинного копипаста, делая код чище, а отладку — приятнее.
Решение особенно эффективно в микросервисной архитектуре, где важно быстро локализовать проблемы, и при работе с командой, где каждая минута, сэкономленная на отладке, на счету.
Интеграция с телеметрией
Важно понимать, что логи — это лишь одна из составляющих полноценной телеметрии приложения наряду с метриками и трейсингом. Однако структурированные логи в связке со структурированными ошибками открывают новые возможности для observability-стека:
Grafana + Loki: Структурированный формат позволяет строить более точные запросы и дашборды. Автоматические поля стека (stack[0]
, stack[1]
) становятся удобными фильтрами для группировки ошибок по местам возникновения.
Алертинг: Постоянная структура метаданных упрощает настройку умных алертов, которые могут анализировать не только факт ошибки, но и её характеристики.
Аналитика: Накопленные структурированные данные позволяют выявлять паттерны в ошибках, горячие точки в коде и тренды деградации производительности.
Таким образом, инвестиции в качественное логирование на уровне кода окупаются на уровне всей инфраструктуры мониторинга.
Полная реализация и её наглядное применение в репозитории пет-проекта
Disclaimer: Предлагаемое решение не претендует на роль панацеи от всех проблем логирования в Go. Это эксперимент, направленный на упрощение повседневной работы с ошибками. Автор открыт для обратной связи и предложений по дальнейшему развитию идеи.