---
title: "LogValuer: отложенные и «опасные» значения"
description: "Интерфейс slog LogValuer: ленивые атрибуты go, метод LogValue и ResolveValue, маскирование токенов и дорогие значения без правки каждого вызова."
course: "Логирование в Go: структурные логи на slog"
url: "https://golangmaster.ru/courses/slog/lessons/09-logvalue/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# LogValuer: отложенные и «опасные» значения

*Курс «Логирование в Go: структурные логи на slog», урок 9 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/slog/lessons/09-logvalue/*

Интерфейс slog LogValuer откладывает вычисление атрибута до момента записи: ленивые атрибуты go нужны там, где значение дорого собрать или опасно показывать. Разберём метод LogValue и ResolveValue, типовую обёртку для маскирования токенов на выходе, связь с уровнями на горячем пути и границы: когда отложенное значение не стоит своих строк.

У обычного атрибута есть нехорошее свойство: значение вычисляется до того, как 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 с пометкой о сбое, и запись не развалится — панику в методе обработчик превращает в строку-заглушку, но лучше до этого не доводить.

### Дорогой дамп считается только при печати

```go
type 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 не тратят время на преобразование. Для записей, которые всё же уйдут, цена — одна конкатенация строк, несравнимая с ценой ротации утечённых ключей и разбора инцидента по тикету, в который дамп кто-то приложил целиком.

### Обёртка-маска для токена

```go
type 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 работу с побочными эффектами — запросы в сеть, записи в файлы, чтение окружения. Отложенное значение должно быть детерминированной функцией от данных, иначе лог начинает зависеть от окружения, а тест — от порядка вызовов.

> **Токен в дампе: одна Any без обёртки.** Кейс: конфиг сервиса хранил токен внешнего 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 короче и прозрачнее.

Связи с другими уроками: [четвёртый урок про атрибуты](/courses/slog/lessons/04-klyuchi-atributy/) объясняет, какие конструкторы закрывают значение до интерфейса, а [девятнадцатый урок про секреты](/courses/slog/lessons/19-sekrety-v-logah/) собирает полный каталог того, что нельзя выводить. Горячий путь и проверку Enabled разбирает [четырнадцатый урок про производительность](/courses/slog/lessons/14-proizvoditelnost/), проверку масок тестами — [двадцатый урок](/courses/slog/lessons/20-testy-logov/), а Handler, который вызывает ResolveValue, — [двенадцатый урок про свой Handler](/courses/slog/lessons/12-svoj-handler/). Карта курса — на [странице курса](/courses/slog/).

### Проверка: момент вызова LogValue

**Вопрос:** Сервис пишет на уровне Info, Debug отключён уровнем обработчика. Вызов slog.Debug("состояние", slog.Any("state", st)) с LogValuer-типом st. Когда выполнится метод st.LogValue()?

- ✅ Не выполнится: запись отброшена до раскрытия атрибутов
- Выполнится сразу: атрибуты вычисляются в момент вызова Debug
- Выполнится, но с задержкой в одну секунду
- Выполнится только при наличии AddSource у обработчика

Раскрытие LogValuer происходит внутри обработчика после проверки уровня: отброшенная запись не тратит время на LogValue. Дорогую работу именно поэтому переносят в метод — она платится только за напечатанные записи.

### Проверка: маскирование

**Вопрос:** type Token string с LogValue, возвращающим звёздочки той же длины. В записи slog.Any("token", tok) при tok равном sk-live-9f3ac2. Что окажется в поле token?

- ✅ Строка из двенадцати звёздочек той же длины
- Исходная строка sk-live-9f3ac2: Any печатает значение как есть
- Пустая строка: секреты отбрасываются обработчиком
- Хэш по умолчанию, который считает стандартная библиотека

Any передаёт обработчику тип Token, ResolveValue вызывает его LogValue, и в запись уходит то, что вернул метод. Без LogValuer-обёртки Any честно напечатал бы исходную строку — в этом и смысл обёртки.

### Что напечатает обработчик?

```go
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))
```

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

- ✅ pass=******* и attempt=3: метод обёртки раскрыл значение при печати
- pass=hunter2: тип с LogValue печатается как обычная строка
- pass пуст: секретные поля вырезаются до уровня звёздочек меньшей длины
- Запись не напечатается: Any с LogValuer требует Enabled

Атрибут pass развернётся в семь звёздочек — ровно по длине hunter2, — а attempt останется обычным числом. Обёртка управляет тем, что уходит в вывод, не мешая самой записи.

### Соберите тип-маску

Допишите метод интерфейса, возврат значения и создание атрибута для типа с LogValue.

```go
type Token string

func (t Token) ___() slog.Value {
        return slog.___(strings.Repeat("*", len(string(t))))
}

slog.Info("аутентификация", slog.___("token", tok))
```

Подсказка: Интерфейс требует LogValue, готовую строку оборачивает StringValue, а тип с методом передают через Any.

Правильные ответы в пропусках: `LogValue`, `StringValue`, `Any`

### Тип-маска от определения до вызова

Расставьте строки по порядку: тип, начало метода, возврат значения, конец метода, создание значения, запись с ним.

Правильный порядок строк:

1. `type Token string`
2. `func (t Token) LogValue() slog.Value {`
3. `return slog.StringValue(strings.Repeat("*", len(string(t))))`
4. `}`
5. `tok := Token("sk-live-9f3ac2")`
6. `slog.Info("запрос авторизован", slog.Any("token", tok))`

### Карточки: LogValuer

- **LogValuer** — Интерфейс с методом LogValue() slog.Value: тип сам решает, каким значением предстать в записи.
- **момент вызова** — LogValue выполняется обработчиком для записи, прошедшей уровень: отброшенные записи не раскрывают атрибуты.
- **ResolveValue** — Функция slog, раскрывающая LogValuer-значения до обычных; цепочки ограничены защитой от зацикливания.
- **маскирование** — Обёртка-секрет печатает звёздочки, хэш или первые символы: одна точка контроля вместо дисциплины в сотнях вызовов.
- **Any против String** — slog.Any оставляет интерфейсу шанс сработать; String и Int закрывают значение заранее — LogValue не вызовется.
- **когда не нужен** — Для готовых и безопасных полей: Int, String, Duration короче и прозрачнее, чем тип с методом.

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

- LogValuer откладывает вычисление атрибута до обработчика: дорогие значения платятся только за напечатанные записи.
- Атрибуты для таких типов создают через slog.Any — типизированные конструкторы закрывают значение заранее.
- slog.ResolveValue раскрывает обёртки, включая цепочки, с защитой от зацикливания.
- Обёртка-маска вокруг секрета печатает звёздочки или хэш: тип контролирует вывод вместо дисциплины вызовов.
- Панику в LogValue обработчик превращает в заглушку, но тяжёлую работу делают без паник.
- Для готовых и безопасных полей интерфейс излишен: обычные конструкторы короче и прозрачнее.

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

Заведите локально тип Secret с LogValue, печатающим первые два символа и звёздочки по остальной длине, и проверьте вывод на токенах разной длины, включая пустую строку. Затем добавьте тип State с GroupValue внутри и убедитесь через LevelWarn у обработчика, что Debug-запись со State не тратит время на сбор — поставьте печать маркера в метод и понаблюдайте.

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

### LogValuer или просто не логировать секрет?

Не логировать — дисциплина, которая ломается в спешке инцидента; обёртка — механика, проверяемая компилятором: секрет живёт в собственном типе и в лог попадает уже преобразованным. Оба подхода сочетают: тип-маска для обязательных полей и запрет на вывод остального — в девятнадцатом уроке.

### Можно ли вернуть из LogValue несколько полей?

Да: верните slog.GroupValue(attrs...) — атрибут развернётся в группу, как slog.Group из седьмого урока. Так печатают состояние структур: map с очередями превращается в подобъект с полями по ключам, а на текстовом формате — в пути с точками.

### Что произойдёт при панике внутри LogValue?

Обработчик поймает панику и заменит значение строкой-заглушкой: запись сохранится, процесс не упадёт. Рассчитывать на это не стоит — тяжёлая работа в LogValue должна завершаться ошибкой, которую можно напечатать, а не паникой, которую придётся ловить.
