---
title: "Условные поля и Any"
description: "Условное логирование в zerolog: собираем событие по условию, zerolog any и его цена, bool-хелперы, Enabled() для дорогих значений, Dict и RawJSON."
course: "Zerolog в Go: быстрое JSON-логирование"
url: "https://golangmaster.ru/courses/zerolog/lessons/07-uslovnye-polya/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Условные поля и Any

*Курс «Zerolog в Go: быстрое JSON-логирование», урок 7 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/zerolog/lessons/07-uslovnye-polya/*

Условное логирование в zerolog — про события, состав которых зависит от данных: email есть не всегда, ошибка не всегда, payload тяжёлый. Разберём сборку события по условию через промежуточную переменную, цену zerolog any, bool-хелперы и guard Enabled(), который не даёт считать дорогие значения впустую.

Событие редко знает о себе всё заранее. У профиля может не быть email, у операции — ошибки, у запроса — заголовков авторизации, и первое, что делает человек на zerolog, — пишет пустые значения на всякий случай: email с пустой строкой, error с нулём, код со статусом 0. Через неделю в системе сбора появляется фильтр «профили без email», который считает записи email="", а вместе с ними — половину записей, где email был, но кавычки потерялись на пути. Дашборд по коду ответа радостно рисует нули, хотя нулей в трафике не было. Мусорные ключи размывают самое ценное свойство структурированного лога: отсутствие значения — это тоже данные, и запись без ключа email говорит правду, а запись с email="" — нет. Вторая крайность — ветвления на каждый вызов: писать одно событие для удачного случая и другое для неудачного, дублируя общий каркас. В zerolog для обеих крайностей есть аккуратное решение: событие собирается пошагово, и каждый шаг можно сделать по условию, а для ошибок условность встроена прямо в библиотеку. Ни пустых значений, ни дублей каркаса — и ровно один способ объяснить системе сбора, почему у записи нет ключа. Дальше разберём механику пошаговой сборки, цену универсального Any и guard, который не даёт считать тяжёлые значения для записей, которые всё равно не уедут.

### Условное логирование: собираем событие по условию

Каждый метод уровня в zerolog возвращает *zerolog.Event — мутируемый аккумулятор записи. Цепочка из одного урока про поля — это просто короткая форма; ничто не мешает разбить её на шаги и пропускать шаги по условию. Паттерн условного логирования выглядит так: evt := logger.Info(), затем evt.Str("user", id), затем сколько угодно проверок с evt.Str или evt.Err внутри if, и в конце — evt.Msg(...), который завершает сборку и пишет запись. До Msg ничего никуда не уходит: буфер события живёт в памяти, порядок полей в JSON остаётся порядком вызовов, а пропущенные шаги не оставляют в записи следов. На практике паттерн применяется шире, чем кажется: условным бывает не только поле, но и уровень — единая точка сборки позволяет выбрать Info или Warn до добавления полей, и каркас при этом один. Часть условий zerolog делает сам: Err и AnErr молча пропускают nil — запись с ошибкой и без неё собирается одной и той же цепочкой, и условие писать не нужно. Errs устроен иначе: он пишет слайс ошибок массивом под заданным ключом, и nil-элементы внутри этого массива сериализуются как null. Поэтому для одиночной необязательной ошибки Err и AnErr остаются главным инструментом, а Errs берут, когда ошибок действительно несколько и их состав известен заранее. Для всего остального — строки, числа, флаги — условие на поле пишется обычным if, и это честная плата за то, что в записи оказываются только существующие значения. Различие между типизированными методами и универсальным Any, который часто тянется сюда же, — тема следующего раздела; база из [урока про поля события](/courses/zerolog/lessons/04-polya-sobytiya/) предполагается знакомой.

### Одно событие, два варианта полей

```go
package main

import (
        "errors"
        "os"

        "github.com/rs/zerolog"
)

type profile struct {
        ID    string
        Email string
}

func save(logger zerolog.Logger, p profile, err error) {
        evt := logger.Info()
        evt.Str("user", p.ID)
        if p.Email != "" {
                evt.Str("email", p.Email)
        }
        if err != nil {
                evt.AnErr("save_error", err)
        }
        evt.Msg("profile processed")
}

func main() {
        logger := zerolog.New(os.Stdout).With().Timestamp().Logger()

        save(logger, profile{ID: "u-1", Email: "ann@example.com"}, nil)
        save(logger, profile{ID: "u-2"}, errors.New("email not verified"))
}

// Output:
// {"level":"info","time":"...","user":"u-1","email":"ann@example.com","message":"profile processed"}
// {"level":"info","time":"...","user":"u-2","save_error":"email not verified","message":"profile processed"}
```

Обе записи собраны одной функцией: каркас события — Info, user, message — общий, а email и save_error добавляются только при наличии значения. AnErr с nil пропустил бы ключ сам, но явный if читается одинаково для строк и ошибок, поэтому в этой функции он у всех полей. В JSON нет ни пустых строк, ни нулей: отсутствие email в первом вызове видно по отсутствию ключа — именно этого хотят фильтры и дашборды.

### zerolog any: цена универсального поля

Метод zerolog any — обёртка над Interface: он принимает значение произвольного типа и маршалит его рефлексией, через тот же json.Marshal, что и стандартная библиотека. Отсюда цена: типизированные поля вроде Str и Int дописывают в буфер готовые байты без единой аллокации, а Any проходит по значению рефлексией и аллоцирует — на порядок и больше, в зависимости от глубины структуры. Есть и обходной путь: если тип реализует интерфейс LogObjectMarshaler с методом MarshalZerologObject, Any направит его в Object, который пишет поля без рефлексии. Когда Any оправдан: разовые дампы на Debug — слепок конфига, содержимое сообщения из очереди, паника с картой состояния. Когда нет: горячий путь, где типы полей известны на этапе компиляции — там Str, Int и Float64 и дешевле, и честнее по типам в системе сбора. Отдельный случай — значение уже сериализовано где-то ещё: RawJSON принимает байты готового JSON и дописывает их в запись как есть, без повторного маршалинга; валидировать байты приходится вам — zerolog вставит их в запись доверчиво. Оценка цены на конкретных числах — тема [урока про производительность](/courses/zerolog/lessons/13-log-proizvoditelnost/), там Any и типизированные поля встают в один бенчмарк.

Для дорогих значений есть guard точнее, чем уровень: метод Enabled(). Вызов if evt := logger.Debug(); evt.Enabled() возвращает событие только если оно переживёт фильтр — глобальный уровень, уровень логгера или сэмплер из урока о прореживании — и внутри блока можно спокойно строить тяжёлый аргумент: собирать слепок, читать статистику, маршалить карту. Сравните с наивной формой logger.Debug().Any("dump", snapshot()).Msg(...): аргумент snapshot() вычисляется при каждом вызове, до того как zerolog поймёт, что Debug отключён, — затраты на построение слепка никуда не деваются, экономится только сериализация. Это общее правило Go: аргументы вычисляются до вызова метода, и логгер не может отсрочить вашу функцию. Enabled() возвращает false и для nil-события, поэтому guard безопасен и в ветках, где уровень неизвестен заранее. Точной формулировки держитесь такой: отключённая запись должна стоить не больше единиц наносекунд — значит, всё, что дороже, обязано жить за Enabled(). Тот же guard работает и для средних по цене полей: конкатенация больших строк или поиск по карте на горячем пути тоже лучше прятать — микросекунды на каждый вызов на пике превращаются в процент CPU.

### Правила условных полей одним списком

- значения нет — ключа нет: пустая строка и ноль в записи врут фильтрам и дашбордам
- ошибки добавляйте Err и AnErr: nil они пропускают сами, условие писать не нужно
- опциональные строки — под if s != ""; счётчики с нулём — либо всегда, либо if n > 0, но по одному правилу в сервисе
- Any — для Debug-дампов и единичных мест; на горячем пути — типизированные поля
- дорогой payload — только за if evt.Enabled(): аргументы вычисляются до вызова метода
- готовый JSON сериализуйте один раз и вставляйте RawJSON, а не прогоняйте через Any второй раз
- булевы флаги — Bool, а не строки "true" и "yes": тип поля важен для фильтров

### Bool-хелперы и честные флаги

Bool(key, b) дописывает в запись JSON-значение true или false — тип события в системе сбора станет булевым, и фильтр admin=true отработает без приведений. Вокруг этого метода выросла маленькая дисциплина, потому что флаги логируют тремя разными способами. Первый — Bool: честно, типизировано, фильтруется везде. Второй — строка: Str("admin", "1") или "yes" — тогда каждый потребитель описывает своё преобразование, а админство внезапно оказывается и строкой, и числом в разных местах логов. Третий — числовые маяки вроде Int("admin", 1): они конфликтуют с настоящими счётчиками, у которых 0 и 1 — значения, а не флаги. Я за Bool везде, где значение действительно бинарное, и за именование без отрицаний: флаг not_admin=true читается на инциденте как ловушка, admin=false — мгновенно. Хелперы есть и на контексте: Context.Bool задаёт флаг всем событиям логгера — например, dry_run для режима примерки, — а Bools добавляет слайс булевых значений целиком. По цене Bool не отличается от Str: один append байтов, ноль аллокаций. И последнее: bool-хелперы закрывают и обратную задачу — читать флаги из конфига в контекст, поэтому признак окружения или фича-флага задаётся один раз на base и едет во все записи сервиса без единого повторного вызова.

> **Живое место: payload, который доехал до прода.** История из типового ретро: для отладки добавили logger.Info().Any("payload", req) — полный объект запроса с токенами и персональными данными. Через месяц аудитория логов выросла втрое, в систему сбора доехали пароли в открытом виде, а вычистить их из ретеншена уже нельзя. Лечится тремя правилами: Any — только на Debug и только за Enabled(); на Info — узкие поля нужных значений, а не объект целиком; на ревью Any в Info, Warn и Error — красный флаг, требующий объяснения. Токены и секреты — отдельная тема, ей посвящён [урок о полях и секретах](/courses/zerolog/lessons/18-log-polya-sekrety/).

### Каким методом добавить значение

| Метод | Когда подходит | Цена | Особенность |
| --- | --- | --- | --- |
| Str, Int, Bool, Float64 | тип известен на этапе компиляции | базовая: без аллокаций | тип в JSON честный |
| Any / Interface | тип значения произвольный, дампы | рефлексия и аллокации | LogObjectMarshaler уводит в Object |
| Object / EmbedObject | структура реализует LogObjectMarshaler | как у типизированных полей | поля структуры пишутся вручную |
| RawJSON | JSON уже собран другой системой | почти нулевая | байты вставляются без валидации |
| Dict | вложенный объект из цепочки | как у полей | группировка без своей структуры |
| Err / AnErr | ошибка, которой может не быть | базовая | nil пропускает ключ автоматически |

### Сборка события как код: Dict и Fields

Как только событие стало последовательностью вызовов, к нему применимы обычные конструкции языка. Вложенность собирает Dict: logger.Info().Dict("http", zerolog.Dict().Str("method", "POST").Int("status", 201)).Msg(...) — внутри записи появится объект http с полями method и status; Dict не требует собственной структуры и живёт целиком в вызове. Слайд значений добавляет Array с zerolog.Arr() или типом, реализующим LogArrayMarshaler. Набор пар, известный только в рантайме, — Event.Fields: он принимает map или слайс чередующихся ключей и значений и добавляет их все разом; ключи карты сортируются по алфавиту, слайс — в порядке следования. Это удобно для метаданных, которые пришли конфигом или ответом системы: не размечать каждое поле руками. Держите рамку: условная сборка — это то же самое событие с тем же контрактом ключей, а не способ менять схему записи от вызова к вызову. Потребители логов должны знать заранее, какие ключи вообще бывают в записях этого логгера, поэтому набор условных полей фиксируют в документации модуля так же, как фиксировали бы схему ответа API. Дёшево и полезно сверяться со списком ключей на ревью: пять минут против сюрприза в системе сбора через месяц.

### Enabled, Any и Dict в одной программе

```go
package main

import (
        "os"

        "github.com/rs/zerolog"
)

func main() {
        logger := zerolog.New(os.Stdout).With().Timestamp().Logger()

        if evt := logger.Debug(); evt.Enabled() {
                evt.Any("payload", map[string]int{"items": 3, "retries": 1}).
                        Msg("debug dump")
        }

        logger.Info().
                Dict("http", zerolog.Dict().Str("method", "POST").Int("status", 201)).
                Msg("order created")
}

// Output:
// {"level":"debug","time":"...","payload":{"items":3,"retries":1},"message":"debug dump"}
// {"level":"info","time":"...","http":{"method":"POST","status":201},"message":"order created"}
```

Guard Enabled() пропускает сборку дамп-события только при включённом Debug: поставьте глобальный уровень Info — и карта вообще не построится. Any сериализует карту рефлексией, и encoding/json выводит ключи карты отсортированными. Info-запись показывает Dict: объект http собран цепочкой без собственной структуры, вложенные поля стоят внутри объекта в порядке добавления.

> **Проверить условную сборку без zerolog.** Песочница сайта — тематическая программа на чистом Go: она собирает JSON-событие вручную и пропускает поля по условию, показывая механику без сторонних модулей. Настоящие Any, Enabled и Dict запускайте локально: go mod init demo, go get github.com/rs/zerolog/log, go run — и сравните вывод двух вызовов из этого урока с разными уровнями.

### Что дальше по курсу

Условные поля стоят вплотную к трём темам курса. Основа — типизированные методы из урока про поля события: условная сборка использует их же, просто под if. Экономика подсказана в уроке про производительность: там Any и Enabled встречаются в бенчмарках, и видно, сколько наносекунд стоит универсальное поле. А если поток записей с условными полями разрастается в спам — например, ретраи с полным payload на каждой попытке, — прореживание разберёт [урок про sampling](/courses/zerolog/lessons/14-sampling/). Практический тест после этого урока: откройте свои вызовы логирования и найдите записи, которые пишут пустые строки и нули. Их нет? Отлично. Есть — вы знаете, что с ними делать: убрать пустые значения под условие, ошибки собрать через AnErr и перепроверить, что ни один тяжёлый payload не считается зря без Enabled.

### Проверка: условные ошибки

**Вопрос:** Какой метод добавит поле только тогда, когда ошибка не nil?

- ✅ Err и AnErr: nil они пропускают молча
- Any: он проверяет ошибку внутри
- Str("error", err.Error()): условие встроено
- Errs: он единственный умеет в nil

Err и AnErr проверяют значение на nil и не добавляют ключ: цепочка не меняется от того, была ошибка или нет. Errs — это массив ошибок под заданным ключом, а не условный метод для одиночной ошибки. Вариант с err.Error() не просто бесполезен — на nil он запаникует при вызове метода.

### Проверка: цена Any

**Вопрос:** Чем Any(key, value) отличается от Str(key, value)?

- Any пишет строку без кавычек в JSON
- ✅ Any маршалит значение рефлексией через json.Marshal — с аллокациями
- Any быстрее и дешевле Str
- Any работает только с числами и картами

Any — обёртка Interface: значение произвольного типа сериализуется рефлексией, что дороже типизированных полей и аллоцирует. Исключение — значение с LogObjectMarshaler: тогда Any уводит его в Object без рефлексии.

### Что выведет Err(nil)?

```go
logger.Info().Str("op", "sync").Err(nil).Msg("done")
```

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

- ✅ Запись без ключа error: nil пропущен
- Запись с полем "error":""
- Запись с полем "error":null
- Панику: nil нельзя передавать в Err

Err и AnErr проверяют ошибку на nil и не добавляют ключ вовсе. Запись выходит такой же, как без вызова Err: level, op, message — и никаких пустых значений.

### Сколько раз вызовется snapshot()?

```go
zerolog.SetGlobalLevel(zerolog.InfoLevel)

if evt := logger.Debug(); evt.Enabled() {
        evt.Any("dump", snapshot()).Msg("debug")
}
```

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

- ✅ Ноль: событие отсечено уровнем, Enabled() вернул false
- Один: аргумент вычисляется всегда
- Каждый вызов Debug по разу
- Два: Enabled и Any вызывают его сами

Глобальный уровень Info отсекает Debug: Enabled() возвращает false, блок не выполняется, и snapshot() не вызывается ни разу. Именно так guard экономит дорогие значения — в отличие от формы Any("dump", snapshot()) без проверки.

### Соберите условное событие

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

```go
package ___

import (
        "errors"
        "os"

        "github.com/rs/zerolog"
)

func main() {
        logger := zerolog.New(os.___).With().Timestamp().Logger()

        evt := logger.___()
        evt.Str("op", "sync")
        err := errors.___("disk full")
        if err != nil {
                evt.___(err)
        }
        evt.Msg("job finished")
}
```

Подсказка: Пакет — main; вывод — os.Stdout; событие начинает Info; ошибку создаёт errors.New; условное добавление ошибки — AnErr.

Правильные ответы в пропусках: `main`, `Stdout`, `Info`, `New`, `AnErr`

### Пошаговая сборка события

Расставьте строки в правильном порядке: от объявления пакета до финализатора.

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

1. `package main`
2. `import (`
3. `  "os"`
4. `  "github.com/rs/zerolog"`
5. `)`
6. `func main() {`
7. `  logger := zerolog.New(os.Stdout).With().Timestamp().Logger()`
8. `  evt := logger.Info()`
9. `  evt.Str("user", "ann")`
10. `  evt.Msg("profile processed")`
11. `}`

### Карточки: условные поля

- **Условное логирование** — Сборка события пошагово: evt := logger.Info(), поля под if, финализатор Msg. Пропущенные шаги не оставляют в записи следов.
- **Any / Interface** — Поле произвольного типа: значение маршалится рефлексией через json.Marshal с аллокациями. Для типов с LogObjectMarshaler уводит в Object.
- **Enabled()** — Метод события: false, если запись отсечена уровнем или сэмплером. Guard для дорогих значений: snapshot считается только когда запись уйдёт.
- **Err / AnErr** — Добавляют ошибку и молча пропускают nil: одна цепочка собирает записи и с ошибкой, и без. Errs пишет слайс ошибок массивом под заданным ключом — nil внутри массива станет null.
- **Bool** — Булево поле с JSON-типом true/false. Честнее строк "true" и числовых маяков: фильтры и агрегации работают без приведений.
- **RawJSON** — Вставляет готовые JSON-байты как значение поля без повторного маршалинга. Валидность байтов — ответственность вызывающего.

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

- Событие собирается пошагово: evt := logger.Info(), поля под if, финализатор Msg.
- Err, AnErr и Errs пропускают nil: условие для ошибок писать не нужно.
- Any — обёртка Interface: рефлексия и аллокации; на горячем пути — только типизированные поля.
- Guard if evt := logger.Debug(); evt.Enabled() не даёт вычислять дорогие значения для отключённых записей.
- Пустая строка и ноль в поле — ложь фильтрам: значения нет — ключа нет.
- Bool пишет честный JSON true/false; строковые и числовые флаги ломают агрегации.
- Dict собирает вложенный объект в вызове, RawJSON вставляет готовые байты без валидации.

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

Возьмите свой обработчик и соберите одно событие пошагово: обязательные поля сразу, опциональные — под if, ошибку — через AnErr. Запустите два варианта данных и сравните JSON: в записи без опциональных значений ключей быть не должно. Затем добавьте Enabled-guard для тяжёлого слепка: установите SetGlobalLevel(InfoLevel) и убедитесь по отладочной печати, что функция слепка не вызывается. Наконец, замените один строковый флаг на Bool и посмотрите, как изменился JSON.

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

### Когда использовать Any, а когда типизированные поля?

Any — для значений с типом, известным только в рантайме: дампы конфига, содержимое сообщений, отладочные слепки, и лучше на Debug. Типизированные Str, Int, Float64 — там, где тип известен при написании кода: они без аллокаций и дают системе сбора стабильные типы.

### Почему не стоит писать пустые значения в лог?

Пустая строка и ноль — валидные значения, и потребитель не отличит их от настоящих: фильтры и дашборды посчитают мусор как данные. Отсутствие ключа честно говорит, что значения не было. Если пустое значение значимо, придумайте ключ с явным смыслом вместо подмены.

### Enabled() вызывать на каждом событии?

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