Логирование в Go: стандартный log против slog
В Go 1.21 появился пакет log/slog — структурированное логирование в стандартной библиотеке. Разбираем, чем slog лучше старого log, как переключиться и где каждый из них уместен.

Зачем вообще менять log
Пакет log живёт в стандартной библиотеке Go с первой версии. Он умеет одно: писать строку текста с меткой времени в io.Writer. Ни уровней, ни полей, ни JSON-вывода. Когда сервис пишет десятки тысяч строк в минуту, а потом эти строки надо фильтровать в Grafana Loki или Elasticsearch — неструктурированный текст превращается в проблему. Именно поэтому экосистема обросла сторонними решениями: logrus, zap, zerolog. Каждый навязывает свой API, и кодовая база начинает зависеть от конкретной библиотеки на уровне каждой функции.
Пакет log/slog, включённый в стандартную библиотеку начиная с Go 1.21, закрывает главный пробел: структурированный вывод с уровнями, атрибутами и подменяемым бэкендом — без внешних зависимостей.
Что умеет старый log
Пакет log предельно прост. Вот его основные возможности:
- Запись строки через
log.Println,log.Printf,log.Fatalf(с завершением программы) иlog.Panicf. - Настройка префикса (
log.SetPrefix) и флагов форматирования даты, времени и файла (log.SetFlags). - Создание отдельного логгера через
log.New, направленного в произвольныйio.Writer.
Этого хватает для CLI-утилит и прототипов, но для продакшен-сервиса — нет. Нельзя выставить уровень (debug, info, warn, error), нельзя прицепить к записи поля request_id или user_id без ручной конкатенации строк, а вывод в JSON потребует написания собственной обёртки.
Что даёт slog
Пакет log/slog решает именно эти задачи, оставаясь частью стандартной библиотеки (Go 1.21+). Ключевые отличия:
- Уровни логирования —
slog.Debug,slog.Info,slog.Warn,slog.Error. Уровень задаётся при создании хэндлера, записи ниже порога отбрасываются без аллокаций. - Структурированные атрибуты — каждый вызов принимает пары ключ-значение:
slog.Info("order created", "order_id", 42, "total", 119.90). Они попадают в вывод как поля, а не как часть склеенной строки. - Два встроенных хэндлера —
slog.TextHandler(выводkey=value) иslog.JSONHandler(вывод JSON). Оба принимаютio.Writerиslog.HandlerOptions. - Интерфейс Handler — можно написать свой хэндлер или подключить адаптер к
zap/zerolog, не меняя код вызовов. - Группы и вложенные атрибуты —
logger.WithGroup("http")добавит префикс ко всем последующим полям, в JSON это станет вложенным объектом. - Контекстное логирование — методы
InfoContext,ErrorContextи др. принимаютcontext.Context, что позволяет хэндлеру извлекать трейс-идентификаторы из контекста.
Сравнение на примерах
Старый log: простой вывод
// Go 1.21+
package main
import "log"
func main() {
log.SetFlags(log.Ldate | log.Ltime | log.Lshortfile)
log.Println("сервер запущен на :8080")
// Вывод: 2025/07/12 10:00:00 main.go:8: сервер запущен на :8080
}
Всё — плоский текст. Чтобы добавить request_id, придётся вручную форматировать строку.
slog: структурированный JSON
// Go 1.21+
package main
import (
"log/slog"
"os"
)
func main() {
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
logger := slog.New(handler)
slog.SetDefault(logger)
slog.Info("сервер запущен",
"addr", ":8080",
"tls", false,
)
// Вывод: {"time":"...","level":"INFO","msg":"сервер запущен","addr":":8080","tls":false}
}
Каждое поле — отдельный ключ в JSON. Такую строку Loki, Elasticsearch или CloudWatch парсят без дополнительных фильтров.
slog: логгер с постоянными атрибутами
// Go 1.21+
package main
import (
"log/slog"
"os"
)
func main() {
base := slog.New(slog.NewJSONHandler(os.Stdout, nil))
// Добавляем атрибуты, общие для всех записей сервиса
logger := base.With("service", "billing", "version", "1.4.2")
logger.Warn("баланс ниже порога",
"user_id", 770,
"balance", 12.50,
)
// Поля service и version окажутся в каждой записи
}
Таблица: log против slog
| Критерий | log | log/slog |
|---|---|---|
| Уровни логирования | Нет (только Fatal/Panic) | Debug, Info, Warn, Error + пользовательские |
| Структурированный вывод | Нет | JSON и Text из коробки |
| Подменяемый бэкенд | Только через подмену Writer | Интерфейс Handler |
| Контекст (context.Context) | Нет | Да, методы *Context |
| Группы / вложенные поля | Нет | WithGroup |
| Аллокации при отключённом уровне | Строка всё равно форматируется | Нет, запись отбрасывается до форматирования |
| Минимальная версия Go | Любая | 1.21 |
Главное преимущество slog — не формат вывода, а интерфейс Handler. Код приложения вызывает slog.Info, а куда именно пишутся логи — решает конфигурация при старте. Это развязывает бизнес-логику и инфраструктуру логирования так же, как io.Writer развязывает запись и назначение.
Типичные ошибки при переходе на slog
1. Передача нечётного числа аргументов
Вызов slog.Info("msg", "key1", val1, "key2") — нечётное число аргументов после сообщения. slog не паникует, но запишет последний ключ с пустым значением и пометит запись как содержащую ошибку (!BADKEY). Линтер go vet начиная с Go 1.22 предупреждает об этом.
2. Дорогие вычисления в аргументах
Если уровень записи ниже порога, slog не форматирует атрибуты, но значения аргументов вычисляются до вызова — это обычная семантика Go. Тяжёлую сериализацию оборачивайте в slog.LogValuer: метод LogValue() будет вызван только при реальной записи.
// Go 1.21+
type lazyJSON struct {
data any
}
func (l lazyJSON) LogValue() slog.Value {
b, _ := json.Marshal(l.data)
return slog.StringValue(string(b))
}
// Сериализация произойдёт только если уровень >= Info
slog.Debug("payload", "body", lazyJSON{data: bigStruct})
3. Глобальный логгер без SetDefault
Функции верхнего уровня slog.Info, slog.Error и т.д. используют логгер, установленный через slog.SetDefault. Если его не вызвать, записи пойдут через дефолтный TextHandler в stderr — без JSON и без нужного уровня. В main или init вызывайте slog.SetDefault один раз.
4. Игнорирование совместимости со старым log
Вызов slog.SetDefault автоматически перенаправляет вывод стандартного log через хэндлер slog. Это значит, что сторонние библиотеки, использующие log.Println, тоже получат структурированный вывод — но на уровне Info и без атрибутов. Если нужно тонкое управление, создайте мост вручную через slog.NewLogLogger.
Сноска. Handler в контексте slog — это интерфейс с методами Enabled, Handle, WithAttrs и WithGroup. Реализуя его, вы контролируете, куда и в каком формате попадают записи: файл, сеть, буфер в памяти, сторонний агрегатор.
Когда slog не нужен
Не всё требует структурированного логирования:
- Короткоживущие CLI-утилиты — если вывод читает человек в терминале и объём — десяток строк,
log.Printlnили дажеfmt.Fprintf(os.Stderr, ...)проще и понятнее. - Уже работающий zap/zerolog с обвязкой — если вокруг
zap.Loggerпостроена middleware, метрики и ротация, переезд на slog ради «стандартности» не даст выигрыша. Имеет смысл реализовать адаптер: slog-хэндлер, делегирующий в zap, — так новый код пишется через slog, а инфраструктура остаётся.
Что делать дальше
После перехода на slog стоит закрыть три вещи:
- Настроить уровень через переменную окружения — парсить строку вроде
LOG_LEVEL=debugпри старте и передавать вHandlerOptions.Level. Так уровень меняется без пересборки. - Добавить request_id из контекста — написать middleware, кладущий идентификатор в
context.Context, и кастомный хэндлер (или обёртку), извлекающий его вHandle. - Проверить линтером —
go vetв Go 1.22+ ловит нечётное число аргументов slog. Включите его в CI, если ещё не включён.
Официальная документация пакета — удобная отправная точка для изучения интерфейса Handler и написания собственных реализаций.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: Пакет log/slog — официальная документация Go, Structured Logging with slog — официальный блог Go
Частые вопросы
Можно ли использовать slog в проекте на Go 1.20 и ниже?
slog полностью заменяет zap и zerolog?
Что будет, если передать нечётное число аргументов в slog.Info?
Как писать логи одновременно в файл и в stdout?
Переход на slog сломает библиотеки, использующие стандартный log?
Как добавить request_id из context.Context в каждую запись slog?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.