Структурированное логирование в Go: сравниваем варианты
log/slog в стандартной библиотеке, zap и zerolog — три способа писать структурированные логи в Go. Разбираем, чем они платят за скорость и когда что выбирать.

Вы разбираете инцидент в проде: сервис отдавал 500 в течение десяти минут, и нужно понять, на каких запросах. В логах вида log.Printf("error processing request: %v", err) вы будете грепать текст регулярками и всё равно не сопоставите ошибку с конкретным пользователем или trace ID. Структурированный лог — это не строка, а набор пар ключ-значение, которые парсер (Loki, Elasticsearch, CloudWatch) разложит по полям и по которым можно фильтровать. В Go есть три живых варианта: log/slog из стандартной библиотеки (с Go 1.21), go.uber.org/zap и github.com/rs/zerolog. Ниже — чем каждый платит за свою скорость и по какому критерию выбирать.
Почему обычный log не годится
Пакет log из стандартной библиотеки пишет форматированную строку. Когда таких строк миллионы в день, а вам нужно найти все запросы с status=500 и user_id=42, вы упираетесь в две проблемы: текст неоднородный (в одном месте user 42, в другом userID=42), и разбор регулярками ломается при любом изменении формата. Структурированный лог решает это на уровне записи: поле user_id всегда называется одинаково и всегда лежит отдельно от сообщения.
Пример на log/slog (Go 1.21+):
package main
import (
"log/slog"
"os"
)
func main() {
// JSON-хендлер пишет каждую запись как объект JSON в stderr
logger := slog.New(slog.NewJSONHandler(os.Stderr, nil))
logger.Info("request handled",
"user_id", 42,
"status", 500,
"path", "/api/orders",
)
}
На выходе — строка JSON с полями time, level, msg, user_id, status, path. Её сразу проглотит любой сборщик логов.
Три кандидата
log/slog — стандартная библиотека
Появился в Go 1.21 как часть log/slog. Главный плюс — ноль зависимостей и единый интерфейс slog.Handler, под который постепенно подстраиваются остальные. Вы описываете логику через Logger, а бэкенд (JSON, текст, свой формат) подключаете хендлером. Есть контекстные методы: logger.InfoContext(ctx, ...) — удобно тянуть trace ID из контекста.
Цена: slog универсален, а не максимально быстр. Аргументы вида "user_id", 42 — это ...any, и каждое значение упаковывается в интерфейс, что даёт аллокации. Типизированный вариант slog.Int("user_id", 42) аллокаций почти не даёт, но выглядит многословнее.
zap — от Uber
Библиотека go.uber.org/zap сделана вокруг идеи «ноль аллокаций на горячем пути». У неё два API: быстрый типизированный (zap.Int, zap.String) и удобный SugaredLogger с ...any, который медленнее, но ближе к привычному Printf. Быстрый путь работает на предвыделенных буферах и почти не трогает сборщик мусора.
package main
import "go.uber.org/zap"
func main() {
logger, _ := zap.NewProduction() // JSON, уровень Info и выше
defer logger.Sync() // сбрасывает буфер перед выходом
logger.Info("request handled",
zap.Int("user_id", 42),
zap.Int("status", 500),
zap.String("path", "/api/orders"),
)
}
Цена: внешняя зависимость и более многословный API на горячем пути. Про defer logger.Sync() легко забыть, и тогда последние записи не попадут в вывод при аварийном завершении.
zerolog — от rs
Библиотека github.com/rs/zerolog пишет JSON напрямую в буфер через цепочку вызовов, без промежуточных структур полей. Синтаксис — билдер:
package main
import (
"os"
"github.com/rs/zerolog"
)
func main() {
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
logger.Info().
Int("user_id", 42).
Int("status", 500).
Str("path", "/api/orders").
Msg("request handled")
}
Цена: билдер-цепочка непривычна и легко «потерять» вызов Msg() в конце — без него запись не запишется. JSON здесь по сути обязателен: человекочитаемый вывод есть (ConsoleWriter), но он заметно медленнее и предназначен для локальной разработки, не для прода.
Что с производительностью
Конкретные наносекунды и аллокации зависят от версии, железа и набора полей — не переписывайте сюда чужие бенчмарки как истину. Замерьте на своём наборе полей сами: положите типичную запись в testing.B и снимите go test -bench=. -benchmem, глядя на колонки ns/op и allocs/op.
func BenchmarkLog(b *testing.B) {
// logger инициализируется один раз, вне цикла замера
b.ReportAllocs()
for i := 0; i < b.N; i++ {
logger.Info("request handled",
slog.Int("user_id", 42),
slog.Int("status", 500),
)
}
}
Порядок величин, который обычно наблюдается: zap и zerolog на типизированном пути дают близкое к нулю число аллокаций, slog с типизированными атрибутами — сопоставимо, slog и sugar-режим zap с ...any — заметно больше. Но для сервиса, который пишет тысячи записей в секунду, а не миллионы, разница в наносекундах на запись обычно теряется на фоне сетевых и дисковых задержек.
Логирование почти никогда не бывает узким местом веб-сервиса. Прежде чем менять библиотеку ради скорости, проверьте профилировщиком, что аллокации на логах вообще заметны на вашем профиле.
Сравнение
| Критерий | log/slog | zap | zerolog |
|---|---|---|---|
| Зависимости | нет, стандартная | внешняя | внешняя |
| Доступен с | Go 1.21 | любая современная | любая современная |
| API | ...any и типизированный | типизированный и sugar | билдер-цепочка |
| Аллокации на горячем пути | малы с типизацией, больше с any | около нуля (быстрый API) | около нуля |
| Контекст (trace ID) | методы *Context | через With | через Ctx/With |
| Риск потерять запись | нет | забыть Sync() | забыть Msg() |
| Единый интерфейс экосистемы | slog.Handler — стандарт | есть адаптер к slog | есть адаптер к slog |
Когда что выбирать
- Новый проект без жёстких требований к скорости логов — берите
log/slog. Ноль зависимостей, стандартный интерфейс, и если упрётесь в производительность, замените хендлер, не трогая код вызовов. - Горячий путь, где логи пишутся миллионы раз в секунду и профиль показывает аллокации на них —
zapс типизированным API илиzerolog. Оба живут в этом сценарии годами. - Уже есть
zapилиzerologв кодовой базе — не мигрируйте ради моды. Оба поддерживают вывод черезslog.Handler, так что новые пакеты можно писать на slog-интерфейсе поверх старого бэкенда. - Библиотека, которую импортируют другие — принимайте
*slog.Loggerилиslog.Handlerв аргументах, а не конкретную реализацию. Так вы не навязываете зависимость пользователям.
Что ломается на практике
- Забытый
Sync()в zap иMsg()в zerolog. В первом случае буфер не сбросится приpanic, во втором запись просто не появится. Проверьте, что критичные ветки завершаются корректно. - Логирование в цикле по горячему пути. Даже быстрая библиотека сериализует поля. Если пишете внутри обработки каждого элемента большого списка — вынесите лог за цикл или поднимите уровень до Debug и отключите его в проде.
- Секреты в полях. Структурированный лог удобно грепать — и злоумышленнику тоже. Не кладите токены, пароли и полные тела запросов в поля. Маскируйте на уровне обёртки логгера.
- Смешение уровней. Ошибка на уровне Info утонет в потоке, а обычное событие на Error поднимет ложную тревогу в алертах. Договоритесь в команде о значении уровней и держитесь его.
- Разные имена одного поля. В одном месте
user_id, в другомuserID— и фильтр в Loki не соберёт всё вместе. Заведите константы для имён ключей.
Что делать дальше
Если проект новый — начните со slog.NewJSONHandler и заведите обёртку, которая добавляет trace ID из контекста в каждую запись. Настройте уровень через переменную окружения, чтобы в проде не сыпал Debug. Затем посмотрите, как ваш сборщик логов (Loki, Elasticsearch, CloudWatch) парсит JSON, и убедитесь, что поля level и time распознаются автоматически. Замеры производительности снимайте только после того, как профилировщик покажет, что логи вообще заметны на профиле — до этого выбор библиотеки решается удобством и зависимостями, а не наносекундами.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: Пакет log/slog — официальная документация Go, Uber zap — репозиторий на GitHub
Частые вопросы
У меня Go 1.20, slog недоступен. Что делать?
Можно ли использовать slog-интерфейс, но zap как бэкенд?
Почему мои логи не появляются при аварийном завершении?
Насколько сильно логирование замедляет сервис в проде?
JSON-логи плохо читаются глазами. Как быть при локальной разработке?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.