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

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

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

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

Ключи и атрибуты: Attr, Int, String и Any

Slog attrs — пары ключ-значение, из которых состоит запись: уровень уже выбран, сообщение придумано, осталось сделать поля удобными для поиска и не навредить себе типами. В уроке — два способа передать атрибут, типизированные конструкторы Int и String, универсальный Any с его ценой, порядок полей и договорённости об именах, без которых структурные логи go рассыпаются на три несмешивающихся озера.

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

Запись лога попадает в хранилище и превращается там в набор полей: по 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.Intintslog.Int("id", 42)
slog.Stringstringslog.String("user", "anna")
slog.Boolboolslog.Bool("paid", true)
slog.Durationtime.Durationslog.Duration("took", 230*time.Millisecond)
slog.Timetime.Timeslog.Time("deadline", t)
slog.Anyлюбое значениеslog.Any("err", err)

Смешанный вызов: пары и Attr рядом

GO
slog.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-выводе появятся два поля с одним именем, что парсеры трактуют по-своему: кто-то возьмёт последнее значение. Поэтому дубликат ключа в одном вызове — ошибка качества, а не фича; ловится она ревью, потому что компилятору безразлично. Практика ведёт себя лучше, когда схема записи стабильна: одно и то же событие печатает одни и те же поля в одном и том же порядке, и дежурный узнаёт строку раньше, чем читает её.

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 хороши тем, что поле добавить дёшево: одна пара в вызове и одна строка в словаре команды, миграции хранилища не нужны. Граница разумного проектирования узкая и честная: обязательные сквозные поля — идентификатор запроса, идентификатор пользователя, длительность, — задают сразу, потому что без них не склеивается история одного запроса; всё остальное появляется по факту поиска. Я не добавляю поле, пока не могу назвать запрос, который по нему ищет, — фантазийные дашборды таким критерием не считаются.

Проверка: вывод пары и Attr

Вывод изменится, если пару "id", 42 заменить на slog.Int("id", 42)?

Проверка: дубликаты ключей

Что напечатает slog.Info("платёж", "id", 42, "id", 43)?

Что выведет программа?

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

items := []int{1, 2}
slog.Info("пакет собран", "items", items, "ok", false)

Соберите вызов с конструкторами

Допишите вызов записи и типизированные конструкторы атрибутов.

slog.("заказ оформлен", "id", 42, slog.("user", "anna"), slog.("paid", true))

Соберите запись с атрибутами по строкам

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

Строки

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

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

Карточки: ключи и атрибуты

Термин

slog.Attr

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

Структура с полями Key и Value — единая форма атрибута. Пары из вызова конвертируются в Attr, конструкторы возвращают его сразу.

1 / 6

Атрибуты разобраны: пары удобны, конструкторы строги, Any точечно спасает, порядок — за автором вызова, а словарь имён — часть контракта команды. Дальше по спирали: в пятом уроке сменим формат вывода и увидим те же атрибуты в JSON; в седьмом сложим их в группы; в восьмом предзададим общие поля логгера, чтобы не таскать request_id через каждый вызов. Домашнее задание ниже — ревизия полей в вашем коде.

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

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

Вывод

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

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

  • Атрибут — Attr с полями Key и Value; пара из вызова конвертируется в него на лету, конструктор возвращает сразу.
  • Вывод у пары и Attr одинаковый; разница в типобезопасности и цене конвертации на горячем пути.
  • Типизированные конструкторы — Int, String, Bool, Duration, Time: единицы измерения зашиты в тип значения.
  • Any — для ошибок и нестандартных типов; поисковые и агрегируемые поля держат на конструкторах.
  • Порядок полей — порядок аргументов; дубликат ключа печатается дважды и ломает JSON-парсеры.
  • Имена ключей — общий словарь в константах: опечатка в строке не ловится ничем, кроме ревью.

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

Возьмите три вызова логирования из своего проекта и перепишите их на конструкторы: числа на slog.Int, длительности на slog.Duration, строки оставьте парами. Затем выпишите все строковые ключи из этих вызовов, проверьте их на дубликаты имён и вынесите повторяющиеся в константы. Напоследок добавьте один Any-атрибут с ошибкой и убедитесь, что значение печатается осмысленно.

Личные заметкиПусто
Хранятся только в вашем браузере (Slog · Ключи и атрибуты: Attr, Int, String и Any)

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

Пары или конструкторы — что выбрать на каждый день?

Для обычного кода достаточно пар: они читаются как продолжение фразы. Конструкторы ставят там, где важна строгость и скорость: значения в цикле, горячий путь, общие обёртки над логгером. Смешивать в одном вызове допустимо, но команда обычно договаривается об одном стиле для типовых записей.

Почему нельзя передавать структуру целиком?

Можно, но запись теряет плоские поля: структура печатается текстом или сворачивается в подобъект, и фильтры по отдельным полям перестают работать. Разложите нужные значения атрибутами, а структуру оставьте для отладочных записей уровня Debug, где это терпимо.

Как назвать ключ — user_id, userID или user?

Как в словаре команды: чаще берут нижний регистр с подчёркиванием — user_id, request_id. Важнее не конкретный стиль, а его единообразие: хранилище не догадывается, что userID и user_id — одно и то же, и строит из них два разных поля.

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

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

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

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