У обычного атрибута есть нехорошее свойство: значение вычисляется до того, как slog решит, будет ли запись вообще напечатана. Вызов slog.Info("состояние", slog.String("dump", buildDump(state))) соберёт дамп даже на уровне Error-only, где Info отбрасывается, — и сделает это в каждой итерации горячего цикла. Вторая беда обратная: значение, которое вычислять дёшево, а показывать нельзя. Токен, пароль или персональные данные попадают в атрибуты честным Any — и уезжают в коллектор вместе с записью. Есть и третий, менее очевидный случай: значение, снятое заранее, устарело к моменту печати — атрибут со срезом состояния описывает прошлое, тогда как обёртка снимает его в момент самой записи, ближе к тому, что увидит дежурный. Обе проблемы решает один интерфейс: LogValuer. Тип, реализующий единственный метод LogValue() slog.Value, превращает свои данные в значение в момент записи, и только если запись действительно уходит в обработчик. Это даёт ленивость для дорогих значений и точку контроля для опасных: обёртка с маскированием пишется один раз, а работает для всех вызовов, где токен уже обёрнут.
slog LogValuer: ленивые атрибуты go#
Интерфейс помещается в одну строку: type LogValuer interface { LogValue() slog.Value }. Разбор slog logvaluer начинается с момента вызова: метод срабатывает не там, где создан атрибут slog.Any("key", v), а внутри обработчика, когда запись уже прошла проверку уровня. Оборачивает его функция slog.ResolveValue: она получает Value, видит внутри тип с LogValue и подставляет результат, повторяя раскрытие, пока не останется обычное значение; от зацикленных цепочек защита встроена в стандартную библиотеку. Для вашего кода это значит: дорогой дамп, посчитанный в LogValue, расходуется только на записи, которые реально напечатались. Атрибут для такого типа создают через slog.Any — типизированные конструкторы вроде String не оставляют интерфейсу шанса сыграть, потому что закрывают значение строкой заранее. Ошибку внутри LogValue тоже можно вернуть честно: метод вправе вернуть slog.StringValue с пометкой о сбое, и запись не развалится — панику в методе обработчик превращает в строку-заглушку, но лучше до этого не доводить.
Дорогой дамп считается только при печати
GOtype State struct {
mu sync.Mutex
queues map[string]int
}
func (s State) LogValue() slog.Value {
s.mu.Lock()
defer s.mu.Unlock()
attrs := make([]slog.Attr, 0, len(s.queues))
for name, depth := range s.queues {
attrs = append(attrs, slog.Int(name, depth))
}
return slog.GroupValue(attrs...)
}
slog.Debug("состояние очередей", slog.Any("state", st))Атрибут state создаётся на каждом вызове, но блокировка мьютекса и сбор списка сработают только у записи, прошедшей уровень: Debug в проде не тронет мьютекс вовсе. GroupValue собирает группы программно — те же вложенные поля, что и slog.Group из седьмого урока, только из данных.
Маскирование на выходе: токен не доезжает#
Второе применение ценнее первого: тип-обёртка вокруг секрета становится единственной точкой, где секрет встречается с логом. Достаточно, чтобы токен существовал в коде как тип Token, а не сырая строка: тогда метод LogValue этого типа печатает звёздочки той же длины, хэш или первые символы, и никакой вызов Info не сможет напечатать значение целиком, не обёртывая его намеренно. Это надёжнее дисциплины «не логируй токен»: договорённости забываются в спешке инцидента, а тип проверяет компилятор. Подробный разбор того, что вообще нельзя выводить и как это соотносится с требованиями к персональным данным, ждёт в девятнадцатом уроке; здесь — механика. Полезно сочетать маскирование и ленивость: обёртка срабатывает на записи, прошедшей уровень, значит дешёвые отброшенные Debug не тратят время на преобразование. Для записей, которые всё же уйдут, цена — одна конкатенация строк, несравнимая с ценой ротации утечённых ключей и разбора инцидента по тикету, в который дамп кто-то приложил целиком.
Обёртка-маска для токена
GOtype Token string
func (t Token) LogValue() slog.Value {
s := string(t)
if len(s) == 0 {
return slog.StringValue("пусто")
}
return slog.StringValue(strings.Repeat("*", len(s)))
}
slog.Info("запрос авторизован",
slog.Any("token", tok),
slog.String("user", login))В записи появится поле token той же длины, что и оригинал, из звёздочек: формат сохраняется для проверки «токен вообще был», а значение — нет. Достаточно, чтобы тип tok был Token: свой LogValue вызовется сам, маскировать руками в каждом вызове не нужно.
ResolveValue и границы отложенности#
Механика раскрытия стоит одной страницы понимания. Когда обработчик собирается писать атрибут, он вызывает slog.ResolveValue: для обычных значений возвращается то же Value, для типа с LogValue — результат метода, при необходимости с продолжением, если внутри снова прячется LogValuer. Глубина цепочки ограничена внутренней защитой от зацикливания, поэтому взаимно ссылающиеся обёртки не повесят процесс — в записи окажется заглушка. Отсюда две границы. Первая: ленивость начинается с Any — атрибуты, созданные через String или Int, уже содержат готовое значение, и LogValue у них не спросит. Вторая: LogValue не отменяет стоимость создания самого атрибута — slog.Any("key", st) обёртывает указатель или структуру мгновенно и дёшево, но если вы для него предварительно считаете готовую строку, ленивость уже потеряна по дороге в вызов. Правило простое: в Any уходит сам объект с методом, а вся тяжёлая работа переезжает внутрь LogValue. На горячем пути это сочетается с Enabled из четырнадцатого урока: проверка уровня до сборки полей экономит ещё больше.
Где отложенное значение окупается#
| Ситуация | Что даёт LogValuer | Типовая обёртка |
|---|---|---|
| секрет в атрибуте | маскирование на выходе: звёздочки или хэш | Token, Secret с методом LogValue |
| дорогой дамп | сбор только для записей, прошедших уровень | State с сбором среза в LogValue |
| значение под мьютексом | блокировка лишь при реальной печати | метод с Lock и GroupValue |
| составное значение | подобъект с полями по данным | slog.GroupValue(attrs...) |
Ещё одна точка, где отложенные значения показывают характер, — тесты. Захват логов в память из двадцатого урока получает записи уже после раскрытия: тест видит звёздочки, а не исходный токен, и это удобно проверять утверждением «в выводе нет подстроки исходного значения». Такой тест дешевле целой политики безопасности и работает как страховка: новый атрибут с секретом, забытый без обёртки, падает на первой же проверке. Для дежурных ценность другая: звёздочки сохраняют длину, поэтому по записи видно, что токен был и его формат совпал с ожидаемым, — вопрос «а был ли там вообще заголовок авторизации» снимается без запроса к инженеру. И последнее: не стоит складывать в LogValue работу с побочными эффектами — запросы в сеть, записи в файлы, чтение окружения. Отложенное значение должно быть детерминированной функцией от данных, иначе лог начинает зависеть от окружения, а тест — от порядка вызовов.
Кейс: конфиг сервиса хранил токен внешнего API строкой, и при отладке подключения её передали в лог как slog.Any("token", cfg.Token) — однажды, «только на стенде». Стендовый вывод писался в тот же файл, что и прод, запись уехала в коллектор и осела в дампе, приложенном к тикету внешней командой. Ротация ключа заняла вечер, а разбор — неделю: приходилось доказывать, что скомпрометирован один токен, а не всё хранилище. Мораль: секрет живёт в типе с LogValue с первого дня, а не после первого инцидента; и тест из двадцатого урока проверяет, что в JSON-выводе ни одно поле не содержит исходного значения.
Когда LogValuer не нужен#
Честная граница: обёртки пишут для значений с поведением — дорого посчитать, опасно показать или неудобно сериализовать. Для обычных полей интерфейс лишний: slog.Int("port", 8080) короче и читается без прыжка в определение типа. Не нужен он и там, где значение уже готово и безопасно: длительность, счётчик, код ответа. Есть и цена у самого приёма: читатель вызова Info больше не видит, что именно напечатается, — за значением надо идти к типу; методы со скрытым поведением хуже методов с явными аргументами, и злоупотреблять этим не стоит. Разумная конвенция проекта: типы с LogValue заводят для секретов, больших дампах и значениях с мьютексом, а список таких типов держат коротким — это документируется рядом со схемой полей. Всё остальное остаётся обычными атрибутами из четвёртого урока: типизированные конструкторы, Any для случаев «вне списка» и группы для вложенности.
Пакета log/slog в песочнице нет — работают fmt, strings, strconv, math, sort, errors и time. Механику маскирования честно разминают на strings: напишите функцию, которая превращает строку в звёздочки той же длины, и сравните её поведение на пустой строке и на коротком токене. Полный LogValuer с ResolveValue запускается локально через go run; интерактивы ниже проверят момент вызова LogValue, поведение обёртки-маски и сборку типа.
Правила LogValuer#
- LogValue вызывается обработчиком для записи, прошедшей уровень: отброшенные записи не считают дампы.
- Атрибут для типа с LogValue создают через slog.Any: String и Int закрывают значение до интерфейса.
- Раскрытием занимается slog.ResolveValue, цепочки ограничены защитой от зацикливания.
- Секреты живут в типе с LogValue с первого дня: обёртка-маска одна, а вызовов записи сотни.
- Панику внутри LogValue обработчик превращает в заглушку — но тяжёлую работу лучше делать без паник.
- Для обычных готовых полей интерфейс не нужен: Int, String и Duration короче и прозрачнее.
Связи с другими уроками: четвёртый урок про атрибуты объясняет, какие конструкторы закрывают значение до интерфейса, а девятнадцатый урок про секреты собирает полный каталог того, что нельзя выводить. Горячий путь и проверку Enabled разбирает четырнадцатый урок про производительность, проверку масок тестами — двадцатый урок, а Handler, который вызывает ResolveValue, — двенадцатый урок про свой Handler. Карта курса — на странице курса.
Проверка: момент вызова LogValue
Сервис пишет на уровне Info, Debug отключён уровнем обработчика. Вызов slog.Debug("состояние", slog.Any("state", st)) с LogValuer-типом st. Когда выполнится метод st.LogValue()?
Проверка: маскирование
type Token string с LogValue, возвращающим звёздочки той же длины. В записи slog.Any("token", tok) при tok равном sk-live-9f3ac2. Что окажется в поле token?
Что напечатает обработчик?
Прочитайте код и выберите, что он выведет:
type Secret string
func (s Secret) LogValue() slog.Value {
return slog.StringValue(strings.Repeat("*", len(string(s))))
}
pass := Secret("hunter2")
slog.Info("вход", slog.Any("pass", pass), slog.Int("attempt", 3))Соберите тип-маску
Допишите метод интерфейса, возврат значения и создание атрибута для типа с LogValue.
Тип-маска от определения до вызова
Расставьте строки по порядку: тип, начало метода, возврат значения, конец метода, создание значения, запись с ним.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: LogValuer
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- LogValuer откладывает вычисление атрибута до обработчика: дорогие значения платятся только за напечатанные записи.
- Атрибуты для таких типов создают через slog.Any — типизированные конструкторы закрывают значение заранее.
- slog.ResolveValue раскрывает обёртки, включая цепочки, с защитой от зацикливания.
- Обёртка-маска вокруг секрета печатает звёздочки или хэш: тип контролирует вывод вместо дисциплины вызовов.
- Панику в LogValue обработчик превращает в заглушку, но тяжёлую работу делают без паник.
- Для готовых и безопасных полей интерфейс излишен: обычные конструкторы короче и прозрачнее.
Домашнее задание
Заведите локально тип Secret с LogValue, печатающим первые два символа и звёздочки по остальной длине, и проверьте вывод на токенах разной длины, включая пустую строку. Затем добавьте тип State с GroupValue внутри и убедитесь через LevelWarn у обработчика, что Debug-запись со State не тратит время на сбор — поставьте печать маркера в метод и понаблюдайте.
Личные заметкиПусто
Вопросы и ответы по уроку
LogValuer или просто не логировать секрет?
Не логировать — дисциплина, которая ломается в спешке инцидента; обёртка — механика, проверяемая компилятором: секрет живёт в собственном типе и в лог попадает уже преобразованным. Оба подхода сочетают: тип-маска для обязательных полей и запрет на вывод остального — в девятнадцатом уроке.
Можно ли вернуть из LogValue несколько полей?
Да: верните slog.GroupValue(attrs...) — атрибут развернётся в группу, как slog.Group из седьмого урока. Так печатают состояние структур: map с очередями превращается в подобъект с полями по ключам, а на текстовом формате — в пути с точками.
Что произойдёт при панике внутри LogValue?
Обработчик поймает панику и заменит значение строкой-заглушкой: запись сохранится, процесс не упадёт. Рассчитывать на это не стоит — тяжёлая работа в LogValue должна завершаться ошибкой, которую можно напечатать, а не паникой, которую придётся ловить.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Миграция с log на slog: мосты и совместимость
- Ошибки в логах: err как атрибут, wrap и детали
- Свой Handler: интерфейс, Record и WithAttrs
- Конкурентность: один логгер на сервис
- With: предзаданные поля и логгер на запрос
- Группы: WithGroup и вложенные поля
- Дефолтный логгер: SetDefault и старый пакет log
- TextHandler и JSONHandler: выбор формата вывода
- Все курсы по Go
Следующая ступень учебного пути
PostgreSQL в Go: драйвер pgx с нуля до продакшена
Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.
- Подключаться к PostgreSQL через pgx и pgxpool
- Сканировать строки в структуры и справляться с NULL
- Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
- Проводить транзакции и массовые вставки через Batch и COPY