Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 11 из 25 ~18 мин +20 XP

Ошибки в логах: err как атрибут, wrap и детали

Разбор сбоя начинается с лога, и план slog ошибки держится на трёх решениях: ошибка идёт в запись атрибутом как есть, цепочка %w сохраняется до места обработки, а детали достают errors.Is и errors.As там, где принимается решение. Разберём, что печатает обработчик для err, что видно из текста цепочки, как свернуть свою ошибку в группу полей через LogValuer и почему дубли строк сбивают алерты.

Содержание урока · 10 разделов

Ошибки чаще других событий попадают в лог и чаще других его портят: err разворачивают в строку через fmt.Sprintf и теряют структуру, логируют на каждом слое пути наверх, а детали — код ответа, путь, лимит — прячутся внутри типа, куда дежурный не заглянет. В slog для ошибок нет отдельного API: ошибка — обычное значение, и всё решают три правила. Первое: err идёт в запись атрибутом как есть, без склеек, — обработчик сам вызовет Error() и напечатает текст цепочки. Второе: детали достают не в момент печати, а в момент обработки — errors.Is для узнавания знакомой ошибки, errors.As для полей своего типа, LogValuer для сворачивания ошибки в группу полей. Третье: запись пишет слой, который принимает решение, — промежуточные слои только оборачивают. В этом уроке — механика атрибута err, что видно из цепочки %w, где брать детали и почему один сбой не должен выглядеть тремя инцидентами. Всё это проверяется одним взглядом на JSON-запись: если в ней есть ключ err с текстом цепочки и структурные поля рядом — разбор инцидента начнётся с ответов, а не с вопросов.

Slog ошибки: err как атрибут записи#

Пара «err», err — конвенция, которую slog не навязывает, но её ждут коллекторы, дашборды и глаза дежурного. Разбор slog ошибки начинается с механики: err в вызове записи превращается в Value типа Any, и штатные обработчики печатают его через Error() одной строкой; JSONHandler пишет текст, если тип не реализует json.Marshaler, TextHandler поступает так же. Из этого следуют три практических вывода. Первый: не склеивайте ошибку в msg — fmt.Sprintf("не удалось: %v", err) кладёт всё в одно поле msg, и фильтр по ключу err перестаёт видеть запись. Второй: nil-ошибку не логируют — пара «err», nil напечатает null и замусорит поток, поэтому ветка записи живёт под if err != nil. Третий: имя ключа фиксируется на весь сервис — err или error, но одно: алерт строится по имени поля, и ошибки с соседним именем пройдут мимо дашборда.

Обёртка %w и запись с err-атрибутом

GO
package main

import (
        "errors"
        "fmt"
        "log/slog"
        "os"
)

func loadConfig() error {
        _, err := os.Open("config.yaml")
        if err != nil {
                return fmt.Errorf("читаю конфиг: %w", err)
        }
        return nil
}

func main() {
        logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

        if err := loadConfig(); err != nil {
                logger.Error("старт не удался", "err", err)
                os.Exit(1)
        }
}

В записи поле err содержит текст всей цепочки: «читаю конфиг: open config.yaml: no such file or directory». Сообщение обёртки и текст исходной ошибки склеились через двоеточие — маршрут сбоя читается сверху вниз, а значение при этом осталось настоящей ошибкой: errors.Is и errors.As работают по нему же.

Цепочка %w: что видно в логе, а что прячется#

Обёртка fmt.Errorf с %w — основа пути ошибки наверх: каждый слой добавляет свой контекст, и Error() возвращает сообщения, склеенные через двоеточие. В логе эта склейка видна: err = «старт сервиса: читаю конфиг: нет файла» читается как маршрут сбоя. Но текст — только поверхность. Под ним живут тип и значение: sentinel для сравнения через errors.Is и поля своего типа через errors.As — в тексте их нет. Поэтому правило такое: текст цепочки идёт в атрибут err для человека, а то, по чему принимается решение и строятся дашборды, — отдельными полями. Пример из жизни: сбой вызова API нёс код 429 и путь, по которому ударили; строка в логе была, но разбирать ретраи было не по чему, пока errors.As не стал доставать тип и поля не уехали отдельными атрибутами. Отдельно про пары глаголов форматирования: %v и %+v сохраняют только текст — цепочка обёрнута ими, и errors.Is с errors.As по ней уже не пройдут, потому что типов в значении больше нет. %w — единственный вариант, когда и текст виден, и машина доберётся до типов. Идиомы wrap со стороны языка уже разобраны в курсе по продвинутому Go — здесь логовая сторона той же истории.

errors.As: поля своего типа в лог

GO
type APIError struct {
        Code int
        Path string
}

func (e *APIError) Error() string {
        return fmt.Sprintf("api %d по %s", e.Code, e.Path)
}

func reportCall(logger *slog.Logger, err error) {
        var apiErr *APIError
        if errors.As(err, &apiErr) {
                logger.Error("вызов не прошёл", "code", apiErr.Code, "path", apiErr.Path, "err", err)
                return
        }
        logger.Error("вызов не прошёл", "err", err)
}

Ветвление по типу даёт структурные поля там, где они есть: код и путь пригодятся для дашборда ретраев, полный текст цепочки остаётся рядом в err. Вторая ветка ловит всё остальное — незнакомые ошибки не пропадают.

LogValuer для error: своя ошибка целиком в полях#

Когда свои типы ошибок ходят по сервису, вызывать errors.As в каждом месте записи утомительно. Выход — интерфейс LogValuer из девятого урока: тип ошибки реализует метод LogValue и возвращает slog.GroupValue с полями. Тогда один вызов slog.Any("err", apiErr) печатает группу code, path, retry_after — без ручного As в вызывающем коде. Обработчик вызывает Resolve в момент записи, поэтому ленивость и маскирование из девятого урока работают и здесь: поле с токеном из ошибки можно спрятать в обёртке. Граница приёма: LogValue описывает стабильную схему полей, а не всё подряд — меняющиеся детали остаются в тексте Error(). Если тип чужой и правок не ждёт, ту же задачу решает обёртка с LogValue поверх него: поля вынимаются один раз в обёртке и печатаются группой.

Где брать детали ошибки#

ДетальКак достатьКогда нужна
текст цепочкиerr.Error() — уже в атрибуте errбыстрый глазами разбор строки
узнавание sentinelerrors.Is(err, sql.ErrNoRows)ветка поведения по известной ошибке
поля своего типаerrors.As(err, &target)код ответа, путь, лимит — атрибутами
место вызова логаAddSource в HandlerOptionsпоиск точки записи в большом сервисе
стек паникиrecover плюс debug.Stack()горутина упала без весточки

Лог ошибок go: один раз там, где обработали#

Правило одного места записи — главное в разборе лог ошибок go: запись пишет слой, который принимает решение — вернуть 500, отложить задачу, уйти в ретрай или завершить процесс. Слой посередине ошибку оборачивает fmt.Errorf с %w и возвращает наверх: он не знает, что с ней сделают, и его строка — только шум. Нарушение правила видно по логам: один сбой — три-пять записей с одинаковым текстом, алерт срабатывает по каждой, дежурный считает инциденты. Отдельно про уровни: Error — для событий, где сервис не смог сделать обещанное; ожидаемые ветки вроде «пользователь не найден» живут на Info или Warn, иначе Error-дашборд показывает нагрузку, а не проблемы. Семантику уровней раскладывает третий урок, а здесь зафиксируем связку: уровень и место записи решаются вместе, в точке обработки.

Стеки и место вызова: AddSource и recover#

slog не пишет стек при каждой записи, и это разумно: стек на Info замусорил бы вывод. Что доступно: опция AddSource у HandlerOptions добавляет в запись поле source с файлом и строкой вызова лога — удобно искать точку записи в большом сервисе, хотя и с ценой на каждую запись. Стек самой ошибки slog не собирает: если он нужен, его приносят обёртки, которые запоминают стек в момент создания ошибки, или debug.Stack() в recover. Горутина без recover погибает молча и роняет процесс — recover ставят в обёртке горутины и пишут slog.Error("паника в горутине", "err", r, "stack", string(debug.Stack())). Точку сбоя в распределённой системе точнее покажет span из трейсера — двадцать второй урок о нём. Плата за AddSource ощутима на горячем пути: обработчик достаёт имя файла и строку на каждую запись, поэтому включают её адресно — на уровне Debug в стенде или на Error в проде, где каждая запись и так редкая.

Правила ошибок в логах#

  • err идёт в запись атрибутом как есть: склейка fmt.Sprintf в msg ломает фильтр по ключу.
  • Имя ключа одно на сервис: err или error — выбор фиксируется и не смешивается.
  • nil-ошибку не логируют: ветка записи живёт под if err != nil.
  • Слой без решения оборачивает fmt.Errorf с %w и возвращает наверх без записи.
  • Запись пишет тот, кто решает судьбу: 500, ретрай, откладка или выход из процесса.
  • Детали достают errors.Is и errors.As в месте обработки или LogValuer у своего типа.

Связи с другими уроками: девятый урок про LogValuer показывает механику отложенных значений и масок, третий урок про уровни — семантику Error. Обработчик с AddSource собирается в двенадцатом уроке про свой Handler, дубли строк как антипаттерн разбирает двадцать третий урок. Идиомы wrap со стороны языка — в уроке про оборачивание ошибок курса по продвинутому Go, база возвратов — в уроке про ошибки и возвраты. Карта курса — на странице курса.

Проверка: err в JSON-записи

slog.Error("вызов не прошёл", "err", err) при JSONHandler. Что окажется в поле err, если тип ошибки не реализует json.Marshaler?

Проверка: где логировать

Репозиторий получил сбой соединения и возвращает ошибку наверх. Где должна появиться запись?

Что окажется в поле err?

Прочитайте код и выберите, что он выведет:

base := errors.New("нет файла настроек")
err := fmt.Errorf("читаю конфиг: %w", base)
err = fmt.Errorf("старт сервиса: %w", err)

logger.Error("запуск прерван", "err", err)

Соберите ветку обработки

Допишите: узнавание sentinel, извлечение типа, уровень записи и обёртку с контекстом.

if err != nil { if errors.(err, sql.ErrNoRows) { return ErrNotFound } var apiErr *APIError if errors.(err, &apiErr) { slog.("вызов не прошёл", "code", apiErr.Code, "err", err) return err } return fmt.("читаю счёт: %w", err) }

Обработка в хендлере

Расставьте строки обработчика по порядку: загрузка, ветка ошибки, запись с полями, ответ клиенту, выход, успех.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: ошибки в логах

Термин

err как атрибут

нажмите, чтобы перевернуть
Определение

Ошибка передаётся в запись как есть: штатные обработчики печатают err.Error() строкой.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • err идёт в запись атрибутом: штатные обработчики печатают err.Error() — текст всей цепочки.
  • Текст цепочки от %w читается как маршрут сбоя, но поля и типы из него не достать.
  • errors.Is узнаёт sentinel, errors.As достаёт поля своего типа — детали идут отдельными атрибутами.
  • LogValuer у своей ошибки печатает группу полей без ручного errors.As в каждом месте.
  • Запись об ошибке пишет слой, который решает судьбу; промежуточные слои только оборачивают %w.
  • Стека по умолчанию нет: AddSource даёт место вызова лога, панику ловит recover с debug.Stack().

Домашнее задание

Соберите локально цепочку из двух обёрток fmt.Errorf с %w и залогируйте её одним вызовом Error с err-атрибутом: проверьте, что текст цепочки виден, а errors.As по этому значению достаёт поля вашего типа. Затем добавьте своей ошибке LogValue с группой полей и сравните JSON-вывод двух подходов.

Личные заметкиПусто
Хранятся только в вашем браузере (Slog · Ошибки в логах: err как атрибут, wrap и детали)

Вопросы и ответы по уроку

Почему err в JSON-логе выглядит строкой, а не объектом?

Штатные обработчики печатают err.Error() — текст цепочки; это держит вывод предсказуемым. Поля своего типа доставайте errors.As и пишите отдельными атрибутами или сверните ошибку в группу через LogValuer — тогда в записи появится объект с полями.

Нужно ли логировать ошибку на каждом слое?

Нет: слой без решения оборачивает fmt.Errorf с %w и возвращает наверх. Запись пишет тот, кто решает судьбу — вернуть 500, уйти в ретрай, отложить задачу. Иначе один сбой даёт три-пять строк, а алерт срабатывает по каждой.

Как в лог попадает стек ошибки?

По умолчанию никак. AddSource добавляет файл и строку вызова лога; для стека ошибки используйте обёртки, запоминающие стек в момент создания, а при панике — recover и debug.Stack(). Стек на каждую запись — дорогой и шумный вариант.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

pgx25 уроков~12 чпрактика25 уроков уже доступны

PostgreSQL в Go: драйвер pgx с нуля до продакшена

Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.

  • Подключаться к PostgreSQL через pgx и pgxpool
  • Сканировать строки в структуры и справляться с NULL
  • Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
  • Проводить транзакции и массовые вставки через Batch и COPY