Запись лога попадает в хранилище и превращается там в набор полей: по id ищут заказ, по user группируют пользователей, по ms строят перцентили. Всё это работает только тогда, когда поля названы договорённо и значения сохраняют типы. В этом уроке разберём оба способа передать атрибут в slog — удобные пары и типизированные конструкторы slog.Int, slog.String, slog.Bool — и честно поговорим про slog.Any: универсальный вход, который спасает на нестандартных значениях и разбалтывает контракт, если им злоупотреблять. Разберём порядок полей, дубликаты ключей и цену, которую платит обработчик за каждую пару.
Slog attrs: пары ключ-значение и конструкторы#
Атрибут в slog — это структура Attr с полями Key и Value. Существует два способа её получить. Первый — удобная пара: пишете "id", 42 прямо в вызове, и slog конвертирует её в Attr на лету при сборке записи. Второй — конструктор: slog.Int("id", 42) возвращает готовый Attr с типизированным значением внутри. На выходе оба варианта дают одинаковые строки: id=42. Разница — в моменте проверки: пара принимает any, поэтому опечатка в числе аргументов или нестроковый ключ обнаружится только при печати — тем самым !BADKEY из второго урока; конструктор возвращает Attr, и компилятор ловит несоответствие типа ещё на этапе сборки. К типизированным конструкторам относятся slog.Int, slog.Int64, slog.Uint64, slog.Float64, slog.String, slog.Bool, slog.Time для моментов времени, slog.Duration для длительностей и slog.Any для всего остального. В одном вызове можно свободно смешивать пары и конструкторы — порядок аргументов сохраняется.
Конструкторы атрибутов#
| Конструктор | Тип значения | Пример использования |
|---|---|---|
| slog.Int | int | slog.Int("id", 42) |
| slog.String | string | slog.String("user", "anna") |
| slog.Bool | bool | slog.Bool("paid", true) |
| slog.Duration | time.Duration | slog.Duration("took", 230*time.Millisecond) |
| slog.Time | time.Time | slog.Time("deadline", t) |
| slog.Any | любое значение | slog.Any("err", err) |
Смешанный вызов: пары и Attr рядом
GOslog.Info("заказ оформлен",
"id", 42,
slog.String("user", "anna"),
slog.Bool("paid", true),
slog.Duration("took", 230*time.Millisecond),
)На выводе всё смешается в один ряд: INFO заказ оформлен id=42 user=anna paid=true took=230ms. Пара и Attr выглядят одинаково, потому что обработчик печатает единый тип — Attr, а пары конвертируются в него до печати. Обратите внимание на Duration: значение печатается как время из пакета time — 230ms, а не «0.23» и не число наносекунд. Для хранилища это значит, что поле took парсится как длительность без догадок о единицах.
Порядок полей и дубликаты ключей#
Порядок атрибутов в записи — это порядок аргументов в вызове: обработчик ничего не сортирует, не переименовывает и не сглаживает. Отсюда два практических следствия. Первое: значимые для поиска поля ставят первыми — идентификатор запроса и идентификатор сущности открывают строку, а длинные детали идут в конце, чтобы в терминале их приходилось прокручивать. Второе: стабильный порядок в типовых записях упрощает чтение диффов и распознавание «своих» строк глазами. С дубликатами slog честен до конца: если вызвать slog.Info("платёж", "id", 42, "id", 43), обе пары попадут в запись — id=42 id=43. Переопределения нет, и в JSON-выводе появятся два поля с одним именем, что парсеры трактуют по-своему: кто-то возьмёт последнее значение. Поэтому дубликат ключа в одном вызове — ошибка качества, а не фича; ловится она ревью, потому что компилятору безразлично. Практика ведёт себя лучше, когда схема записи стабильна: одно и то же событие печатает одни и те же поля в одном и том же порядке, и дежурный узнаёт строку раньше, чем читает её.
Кейс: сервис писал "user_id" в половине файлов и "userId" в другой половине — оба варианта прошли ревью, потому что каждый файл сам по себе выглядел правильно. В хранилище появились два поля, и поиск по пользователю стал зависеть от сервиса-автора. Лечение: имена ключей живут в константах общего пакета, а не в строках вызовов; ещё сильнее прижимает контракт slog.With — предзаданные поля логгера, которые разбираются в восьмом уроке. Опечатку в строке не найдёт ни компилятор, ни тест: контракт полей держится только дисциплиной.
Any и ловушка потерянного ключа
GO// пара не сходится: последнему значению не хватает ключа
slog.Info("платёж завис", "id", 42, "urgent")
// Any принимает всё, включая карты и структуры
slog.Any("payload", map[string]int{"a": 1, "b": 2})Первая запись напечатает id=42 !BADKEY=urgent: значение живо, ключа нет — подробный разбор был во втором уроке. Вторая запись покажет payload=map[a:1 b:2] в текстовом выводе: Any упакует всё, что вы дали. Это и сила, и опасность Any: карта и структура печатаются как fmt рисует их, а в JSON-обработчике превратятся в подобъект, если их содержимое маршаллится. Значение с нестандартным типом без строкового вида может выдать и мусор вроде адреса структуры — поэтому после добавления Any-поля запись проверяют глазами.
Структурные значения — отдельная глава вкуса. Передать структуру целиком через Any можно, но структурные логи go выигрывают от плоских полей: дежурный ищет user_id=42, а не разглядывает вложенный текст. Плоское поле проще индексировать, дешевле хранить и быстрее фильтровать — вложенность нужна реже, чем кажется на старте. Правило, которое прижилось в командах: сущность раскладывают на атрибуты (id, user, amount), а Any оставляют для значений, у которых есть осмысленное строковое представление — ошибка, URL, фрагмент конфигурации. Для ленивых и дорогих значений есть LogValuer — атрибут, который вычисляется в момент печати, а не в момент вызова; он же решает задачу маскирования секретов на выходе. Если хочется вложенности без плоского хаоса — у slog есть группы: префиксы вида request.id=42, о которых рассказывает седьмой урок.
Any: универсальный вход и его цена#
slog.Any("err", err) — самый частый Any в реальном коде, и это здоровый шаблон: ошибка — значение со строковым видом, и атрибут с ней лучше пустого поля. Цена универсальности в другом месте. Пара "key", value хранит значение в any: каждый вызов упаковывает значение в интерфейс, а обработчик распаковывает его и решает, как печатать, — на горячем пути это заметная работа. Конструкторы вроде slog.Int упаковывают значение в специализированный Value без лишней конвертации, поэтому их любят в узких местах; измерения этой разницы — тема четырнадцатого урока. Вторая цена — потеря типа: Any-поле в хранилище перестаёт быть числом или длительностью, если печаталось текстом, и фильтр по диапазону к нему не применишь. Практическое правило: Any — для ошибок и нестандартных типов, типизированные конструкторы — для всего, что участвует в поиске и агрегации.
Правила имён и значений#
- Имена ключей — в нижнем регистре, слова через подчёркивание: user_id, request_id.
- Имя не повторяет название события: у записи «заказ оформлен» поле id, а не order_id оформлен.
- Длительности — slog.Duration, моменты времени — slog.Time: единицы зашиты в тип.
- Сущности раскладываются на плоские поля; Any — для ошибок и строковых значений.
- Ключи живут в константах общего пакета, а не в строках каждого вызова.
- Новое поле появляется после первого реального поиска, а не «на всякий случай».
Спорное мнение: не плодите поля заранее#
Мнение для архитектурного обсуждения: проектирование набора полей впрок приносит больше вреда, чем пользы. Команды любят придумать двадцать атрибутов «для будущего дашборда» — и через месяц половина полей пустует в каждой записи, раздувая объём и внимание, а два реально нужных не появились, потому что никто не думал о них заранее. Структурные логи go хороши тем, что поле добавить дёшево: одна пара в вызове и одна строка в словаре команды, миграции хранилища не нужны. Граница разумного проектирования узкая и честная: обязательные сквозные поля — идентификатор запроса, идентификатор пользователя, длительность, — задают сразу, потому что без них не склеивается история одного запроса; всё остальное появляется по факту поиска. Я не добавляю поле, пока не могу назвать запрос, который по нему ищет, — фантазийные дашборды таким критерием не считаются.
Словарь полей, согласованный в команде, — это API для хранилища: коллектор и дашборды читают именно его. Смена имени ключа ломает фильтры так же, как переименование колонки таблицы ломает запросы. Что кладут в записи для Loki и ELK и какие схемы полей ждут коллекторы — семнадцатый урок; вложенные поля и группы — седьмой.
Пакета log/slog в песочнице сайта нет — доступно подмножество стандартной библиотеки: fmt, strings, strconv, math, sort, errors, time. Механику атрибутов она разминает честно: соберите слайс из пар имя-значение, напечатайте его fmt.Println в порядке добавления, добавьте дубль ключа и увидите обе пары — ровно так же ведёт себя slog. Проверку типов из конструкторов имитируйте структурой с полями Key и Value: компилятор не даст забыть имя. Полный slog — локально через go run.
Проверка: вывод пары и Attr
Вывод изменится, если пару "id", 42 заменить на slog.Int("id", 42)?
Проверка: дубликаты ключей
Что напечатает slog.Info("платёж", "id", 42, "id", 43)?
Что выведет программа?
Прочитайте код и выберите, что он выведет:
items := []int{1, 2}
slog.Info("пакет собран", "items", items, "ok", false)Соберите вызов с конструкторами
Допишите вызов записи и типизированные конструкторы атрибутов.
Соберите запись с атрибутами по строкам
Расставьте строки в порядке выполнения: пакет, импорт, точка входа, две записи с атрибутами, закрывающая скобка.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: ключи и атрибуты
Атрибуты разобраны: пары удобны, конструкторы строги, Any точечно спасает, порядок — за автором вызова, а словарь имён — часть контракта команды. Дальше по спирали: в пятом уроке сменим формат вывода и увидим те же атрибуты в JSON; в седьмом сложим их в группы; в восьмом предзададим общие поля логгера, чтобы не таскать request_id через каждый вызов. Домашнее задание ниже — ревизия полей в вашем коде.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Атрибут — Attr с полями Key и Value; пара из вызова конвертируется в него на лету, конструктор возвращает сразу.
- Вывод у пары и Attr одинаковый; разница в типобезопасности и цене конвертации на горячем пути.
- Типизированные конструкторы — Int, String, Bool, Duration, Time: единицы измерения зашиты в тип значения.
- Any — для ошибок и нестандартных типов; поисковые и агрегируемые поля держат на конструкторах.
- Порядок полей — порядок аргументов; дубликат ключа печатается дважды и ломает JSON-парсеры.
- Имена ключей — общий словарь в константах: опечатка в строке не ловится ничем, кроме ревью.
Домашнее задание
Возьмите три вызова логирования из своего проекта и перепишите их на конструкторы: числа на slog.Int, длительности на slog.Duration, строки оставьте парами. Затем выпишите все строковые ключи из этих вызовов, проверьте их на дубликаты имён и вынесите повторяющиеся в константы. Напоследок добавьте один Any-атрибут с ошибкой и убедитесь, что значение печатается осмысленно.
Личные заметкиПусто
Вопросы и ответы по уроку
Пары или конструкторы — что выбрать на каждый день?
Для обычного кода достаточно пар: они читаются как продолжение фразы. Конструкторы ставят там, где важна строгость и скорость: значения в цикле, горячий путь, общие обёртки над логгером. Смешивать в одном вызове допустимо, но команда обычно договаривается об одном стиле для типовых записей.
Почему нельзя передавать структуру целиком?
Можно, но запись теряет плоские поля: структура печатается текстом или сворачивается в подобъект, и фильтры по отдельным полям перестают работать. Разложите нужные значения атрибутами, а структуру оставьте для отладочных записей уровня Debug, где это терпимо.
Как назвать ключ — user_id, userID или user?
Как в словаре команды: чаще берут нижний регистр с подчёркиванием — user_id, request_id. Важнее не конкретный стиль, а его единообразие: хранилище не догадывается, что userID и user_id — одно и то же, и строит из них два разных поля.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- TextHandler и JSONHandler: выбор формата вывода
- Дефолтный логгер: SetDefault и старый пакет log
- Группы: WithGroup и вложенные поля
- With: предзаданные поля и логгер на запрос
- Уровни логов: Debug, Info, Warn и Error
- Первые строки: slog.Info и вывод по умолчанию
- Знакомство с slog: зачем структурированные логи
- Все курсы по Go
Следующая ступень учебного пути
PostgreSQL в Go: драйвер pgx с нуля до продакшена
Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.
- Подключаться к PostgreSQL через pgx и pgxpool
- Сканировать строки в структуры и справляться с NULL
- Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
- Проводить транзакции и массовые вставки через Batch и COPY