---
title: "Производительность: Enabled и горячий путь"
description: "Slog производительность: проверка Enabled до сборки полей, цена Attr и Any, бенчмарки со своим обработчиком — быстрый логгер go на дисциплине, а не на трюках."
course: "Логирование в Go: структурные логи на slog"
url: "https://golangmaster.ru/courses/slog/lessons/14-proizvoditelnost/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Производительность: Enabled и горячий путь

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

Быстрый логгер начинается не с зависимости, а с вопроса, что именно стоит денег: разбор slog производительность отделяет цену записанных строк от цены отброшенных и показывает, где помогает Enabled, типизированные конструкторы и бенчмарк со своим профилем.

Лог стоит сервису дважды: на записанных строках — сборка Record и сериализация обработчиком, и на отброшенных — вычисление аргументов, которые никто не увидит. Вторая часть обычно удивляет сильнее: вызов Debug с дорогим аргументом на проде с уровнем Info платит за сборку аргумента на каждой итерации, хотя запись летит в корзину. Вопрос производительности лога поэтому не звучит как «быстрый ли slog» — он звучит как «сколько стоит мой вызов с моими полями и моим обработчиком». Ответ складывается из трёх частей: проверка Enabled до сборки дорогих полей, экономные атрибуты вместо структур в Any и честный бенчмарк своего пути. Ни одна из них не требует трюков: это дисциплина вызовов, а не борьба с библиотекой. Разберём каждую часть, посмотрим на ловушки вроде fmt.Sprintf в аргументах и соберём методику измерения, которую можно повторить на своём сервисе. Побочный эффект этой дисциплины — лог читается лучше: меньше склеек, меньше мусорных полей, каждая запись отвечает на свой вопрос.

### Slog производительность: Enabled до сборки полей

Разбор slog производительность начинается с отброшенной записи. Аргументы вызова вычисляются до того, как Logger спросит Handler, нужен ли ему этот уровень, — поэтому logger.Debug("состояние", "dump", buildDump(state)) соберёт дамп на каждой итерации даже там, где Debug отключён. Лечится это плоской проверкой: logger.Enabled(ctx, slog.LevelDebug) возвращает решение обработчика за одно сравнение, и дорогую сборку прячут под неё. Ставить проверку стоит там, где аргументы дорогие: дампы состояния, сериализация структур, запрос счётчиков. Дешёвые пары вида «user_id», id обёртывать не нужно — цена их вычисления сравнима с ценой самой проверки, и guard размажет по коду больше шума, чем сэкономит наносекунд. Отдельный приём — LogValuer из девятого урока: отложенное значение собирается только при записи, и отброшенный вызов его не вызывает; там, где поле дорогое и повторяется во многих местах, обёртка с LogValue чище, чем if на каждом вызове.

### Guard на горячем пути

```go
logger := slog.New(slog.NewJSONHandler(os.Stdout,
        &slog.HandlerOptions{Level: slog.LevelInfo}))

// дорогое поле без guard: дамп собирается и летит в корзину
logger.Debug("состояние", "dump", buildDump(state))

// с guard: buildDump вызывается только при включённом Debug
if logger.Enabled(context.Background(), slog.LevelDebug) {
        logger.Debug("состояние", "dump", buildDump(state))
}
```

Первый вызов платит за buildDump при любом пороге уровня: аргументы вычисляются до проверки. Второй спрашивает обработчик и собирает дамп только тогда, когда запись действительно уйдёт. Порог Info здесь для примера — в стенде он часто стоит на Debug, и guard становится прозрачным.

### Цена Attr: типизированные конструкторы против Any

Значение в slog хранится в Value вместе с типовым kind: slog.Int кладёт число как int64, slog.String — строку, и встроенные обработчики печатают их напрямую, без отражения. Пара с произвольным значением идёт через Any: для структур и срезов это интерфейс и marshalling через отражение в JSONHandler — аллокации и время на каждую записанную строку. Самый частый расход — fmt.Sprintf в аргументах: строка склеивается до проверки уровня, даже если запись отбросят, и это тот случай, когда «бесплатный Debug» стоит как настоящий. Строку, без которой не обойтись, собирают внутри guard'а или выносят в поле отдельным вызовом. Константные атрибуты вроде component=api выносят в переменные пакета: Attr — иммутабельное значение, безопасное для горутин из тринадцатого урока, и переиспользование ничего не стоит. Группы и вложенные поля собирают один раз при старте логгера — на горячем пути они не добавляют скорости, только структуру.

### Бенчмарк своего вызова

```go
func BenchmarkInfo(b *testing.B) {
        h := slog.NewJSONHandler(io.Discard,
                &slog.HandlerOptions{Level: slog.LevelInfo})
        logger := slog.New(h)

        b.ReportAllocs()
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                logger.Info("горячий путь", "user_id", slog.Int(42), "n", i)
        }
}
```

io.Discard поглощает записи без работы с диском — измеряется именно цена вызова и сериализации, а не writer. ReportAllocs показывает аллокации на вызов: их не ноль даже у чистого пути, и резкий рост после правки — сигнал. Так же меряют отброшенный вызов: замените Info на Debug и увидите цену аргументов без записи.

### Бенчмарки: измеряйте свой вызов

Числа из чужих таблиц сравнения логгеров к вашему случаю отношения не имеют: цена вызова зависит от полей, их типов, обработчика и writer. Методика простая и повторяемая: свой обработчик с вашим порогом уровня, свои поля в том сочетании, как они пишутся на горячем пути, io.Discard вместо внешнего мира. Отдельно меряют записанный вызов и отброшенный — второе число отвечает на вопрос, сколько стоит забытый Debug с дорогим аргументом. Бенчмарк отвечает «сколько», но не «где болит»: место лога в общей нагрузке покажет профиль сервиса — иногда наносекунды на вызов теряются в цене HTTP-запроса, а иногда цикл по миллиону элементов дышит логом. Для честного сравнения с zap измеряют ту же комбинацию полей на их логгере — и помнят, что у zap есть sampling, а у встроенных обработчиков slog его нет; при шторме записей помогает уровень и дисциплина полей, а не надежда на фильтр. Полезное соотношение — между двумя своими измерениями: во сколько раз отброшенный вызов дешевле записанного, настолько и тяжёлой была бы забытая отладка на проде.

### Источники расхода и что с ними делать

| Источник | Когда платим | Что делать |
| --- | --- | --- |
| аргументы до проверки уровня | на каждом вызове, даже отброшенном | Enabled-guard или LogValuer для дорогих полей |
| Any со структурой | на каждой записанной строке | раскладывать на типизированные поля заранее |
| fmt.Sprintf в аргументах | на каждом вызове, даже отброшенном | собирать строку под guard'ом или передавать поля |
| сериализация Handler | на каждой записанной строке | меньше полей, типизированные конструкторы |
| writer и сеть | на каждой записанной строке | быстрый writer, буферизация с Flush, сбор на стороне |

### Быстрый логгер go: дисциплина вместо трюков

Собрать быстрый логгер go можно без единой экзотики, на четырёх правилах дисциплины. Первое: уровни по окружению — Info в проде, Debug в стенде; раскладку по переменным окружения даёт восемнадцатый урок, и это самый крупный рычаг: половина записей исчезает без единой строчки кода. Второе: дорогие значения — под Enabled или в LogValuer, чтобы отброшенная запись не платила за сборку. Третье: лог в цикле по элементам — антипаттерн из двадцать третьего урока; агрегируйте итог или логируйте выборочно, иначе даже дешёвый вызов в миллион итераций станет статьёй расходов. Четвёртое: ключи и константные атрибуты — константами пакета, а не строками по месту. Мьютекс обработчика из тринадцатого урока в этой картине не виноват: его цена на фоне сборки записи несущественна, и разъезд логгеров по горутинам её не уберёт, зато потеряет поля.

> **Guard Enabled везде: код утонул в проверках.** Кейс: после прочтения о цене аргументов команда обложила if logger.Enabled(...) каждый второй вызов в сервисе. Код стал нечитаемым, ревью растянулись, а профиль сервиса не сдвинулся: дорогих аргументов в логе было два, остальное — пары с идентификаторами, цена которых сопоставима с самой проверкой. Лечение: guards сняли везде, кроме двух горячих циклов, которые нашёл профиль; дампы переехали в LogValuer; уровень в проде подняли до Info. Проверка результата — тот же бенчмарк до и после: выигрыш на записанном пути измеряется наносекундами, а читаемость не восстановишь.

Как найти места, где guard нужен на самом деле. Первый шаг — профиль: pprof по нагруженному сценарию покажет, дышит ли лог в кадре вообще. Второй — просмотр циклов: вызов записи внутри for по элементам — кандидат на агрегацию или выборочную запись, особенно если внутри вызова склейка строк или сбор структуры. Третий — поиск по fmt.Sprintf рядом с вызовами лога: каждая такая строка платит за склейку до проверки уровня. После правок бенчмарк из урока фиксирует результат: наносекунды и аллокации на вызов до и после. И не забывайте направление: оптимизируют измеренное и профилированное, а не то, что выглядит медленным в коде — лог в HTTP-хендлере тонет в цене запроса, тот же лог в цикле по миллиону позиций определяет профиль целиком. Последняя проверка перед коммитом — глазами: guard вокруг дешёвой пары выглядит как забота, а читает его следующий разработчик, который не видел ваш профиль.

> **Уровень на ходу: slog.LevelVar.** Порог уровня можно менять без пересборки логгера: var level slog.LevelVar; level.Set(slog.LevelInfo); обработчику передают Level: &level. Тесты поднимают порог до Debug на время кейса, сервис по сигналу включает отладку на минуту — восемнадцатый урок строит на LevelVar уровни по окружению. Проверка Enabled с LevelVar тоже честная: она читает текущее значение.

> **Код урока — локально, интерактивы — на странице.** Пакета log/slog в песочнице нет — работают fmt, strings, strconv, math, sort, errors и time. Методику бенчмарка честно разминают на time: замерьте time.Now и time.Since вокруг цикла, который склеивает строку с полями через fmt, и сравните вариант с fmt.Sprintf и вариант с прямыми значениями — разница видна и без testing.B. Полные бенчмарки со своим обработчиком запускаются локально через go test -bench; интерактивы ниже проверят момент вычисления аргументов, цену полей и сборку бенчмарка.

### Правила горячего пути

- Дорогие аргументы прячут за Enabled или LogValuer: отброшенная запись не платит за сборку.
- Дешёвые пары guard'ом не обкладывают: цена проверки сопоставима с ценой вычисления.
- Структуры и срезы в Any раскладывают на типизированные поля до места записи.
- fmt.Sprintf в аргументах убирают: склейка платит до проверки уровня.
- Лог внутри цикла по элементам агрегируют или делают выборочным.
- Решение принимают по бенчмарку и профилю своего сервиса, а не по чужим таблицам.

Связи с другими уроками: [девятый урок про LogValuer](/courses/slog/lessons/09-logvalue/) переносит сбор значения на момент записи, [третий урок про уровни](/courses/slog/lessons/03-urovni-logov/) — база семантики, [восемнадцатый](/courses/slog/lessons/18-urovni-po-env/) — уровни по окружению. Контракт Enabled у своего обработчика описывает [двенадцатый урок](/courses/slog/lessons/12-svoj-handler/), цена мьютекса при конкурентной записи — [тринадцатый](/courses/slog/lessons/13-konkurentnost/), а лог в цикле как антипаттерн разбирает [двадцать третий](/courses/slog/lessons/23-antipatterny/). Методика бенчмарков — в [уроке про benchmarki](/courses/go-advanced/lessons/17-benchmarki/) курса по продвинутому Go, sampling как фильтр штормов — в [уроке про sampling](/courses/zap/lessons/11-sampling/) курса по zap. Карта курса — на [странице курса](/courses/slog/).

### Проверка: цена отброшенной записи

**Вопрос:** Уровень обработчика — Info. Какой вызов дороже всех на горячем пути?

- ✅ logger.Debug("состояние", "dump", buildDump(state)): аргумент вычисляется, хотя запись отбросят
- logger.Debug("состояние", "user_id", id)
- logger.Info("запрос", "user_id", id)
- logger.Debug("состояние")

Аргументы вычисляются до проверки уровня: buildDump соберёт дамп даже для отброшенной записи. Дешёвые пары и вызовы без полей почти бесплатны, дорогие значения прячут за Enabled или LogValuer.

### Проверка: как измерять

**Вопрос:** Как понять, дорого ли стоит лог на горячем пути вашего сервиса?

- ✅ Бенчмарк со своим обработчиком, своими полями и io.Discard: время и аллокации на вызов
- Взять число из чужой таблицы сравнения логгеров
- Посчитать строки лога за день и умножить на средний размер
- Замерить общий uptime сервиса до и после включения логов

Цена вызова зависит от полей, обработчика и writer — чужие числа про другой случай. Бенчмарк с ReportAllocs показывает именно ваш профиль, а место в общей нагрузке — профиль сервиса.

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

```go
logger := slog.New(slog.NewJSONHandler(io.Discard,
        &slog.HandlerOptions{Level: slog.LevelInfo}))

for i := 0; i < 100; i++ {
        if logger.Enabled(context.Background(), slog.LevelDebug) {
                logger.Debug("состояние", "dump", buildDump(state))
        }
}
```

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

- ✅ Ни разу: Enabled отсёк записи до вычисления аргумента
- Сто раз: Debug собирает аргументы до проверки
- Один раз: компилятор выносит вызов из цикла
- Зависит от writer: io.Discard всё равно пишет

Порог Info ниже Debug, Enabled возвращает false, и buildDump внутри guard'а не вызывается вовсе — ровно этого и ждут от проверки на горячем пути.

### Соберите динамический уровень и guard

Допишите: изменяемый уровень, проверку на горячем пути, типизированный атрибут и отчёт аллокаций.

```go
var level slog.___
level.Set(slog.LevelInfo)
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: &level})

if logger.___(ctx, slog.LevelDebug) {
        logger.Debug("состояние", "user_id", slog.___(uid))
}

func BenchmarkInfo(b *testing.B) {
        b.___()
}
```

Подсказка: Уровень на ходу держит LevelVar, решение о записи спрашивает Enabled, число упаковывает slog.Int, аллокации показывает ReportAllocs.

Правильные ответы в пропусках: `LevelVar`, `Enabled`, `Int`, `ReportAllocs`

### Бенчмарк по строкам

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

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

1. `func BenchmarkInfo(b *testing.B) {`
2. `        h := slog.NewJSONHandler(io.Discard, nil)`
3. `        logger := slog.New(h)`
4. `        b.ReportAllocs()`
5. `        for i := 0; i < b.N; i++ { logger.Info("горячий путь", "n", i) }`
6. `}`

### Карточки: производительность

- **цена отброшенной записи** — Вычисление аргументов: они считаются до проверки уровня, дампы платят даже в корзину.
- **Enabled-guard** — logger.Enabled(ctx, LevelDebug) перед дорогой сборкой: плоская проверка вместо сборки полей.
- **LogValuer на горячем пути** — Отложенное значение собирается только при записи: отброшенный вызов его не вызывает.
- **Any против typed** — slog.Int и slog.String печатаются без отражения; структуры в Any тянут marshalling.
- **slog.LevelVar** — Изменяемый уровень на ходу: Set без пересборки логгера, обработчику отдаётся указатель.
- **методика бенчмарка** — Свой обработчик, свои поля, io.Discard: ReportAllocs и сравнение записанного с отброшенным.

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

- Аргументы вычисляются до проверки уровня: отброшенный Debug платит за свою сборку.
- Enabled-guard ставят только под дорогими полями: дампы, сериализация, счётчики.
- LogValuer переносит сбор значения на момент записи и чистит горячий путь без if.
- Типизированные конструкторы печатаются без отражения; структуры в Any — дорогие.
- Бенчмарк со своим обработчиком и io.Discard даёт время и аллокации именно вашего вызова.
- Быстрый логгер — дисциплина: уровни по окружению, поля без склеек, цикл без спама.

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

Соберите локально бенчмарк из урока со своим набором полей и прогоните его go test -bench: зафиксируйте наносекунды и аллокации. Затем добавьте дорогой аргумент через buildDump-подобную функцию и сравните три варианта: без guard, с Enabled и с LogValuer — разницу в отброшенных вызовах видно по бенчмарку.

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

### Насколько slog медленнее zap?

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

### Нужно ли оборачивать проверкой Enabled каждый вызов?

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

### Почему аргументы вычисляются до проверки уровня?

Go вычисляет аргументы функции в месте вызова, а slog.Debug — обычная функция: к моменту вызова dump уже собран. Механизм отложенных значений вынесен в LogValuer — обёртка отдаёт slog.Value, который обрабатчик раскроет только при записи.
