slog в одном примере
log/slog (Go 1.21) пишет структурированные записи: сообщение, уровень и атрибуты ключ-значение.
Каждая строка выходит в виде time=... level=INFO msg="user logged in" user=ada attempts=1. Поскольку каждое значение это отдельное поле, сборщик логов (Loki, Elasticsearch, CloudWatch, Datadog) может фильтровать по user=ada или level=ERROR без регулярных выражений по свободному тексту.
Примеры на этой странице пишут в os.Stdout, чтобы вывод шёл по порядку. В реальном сервисе логи обычно идут в os.Stderr, куда пишет и логгер по умолчанию.
Уровни
| Уровень | Значение | Для чего |
|---|---|---|
slog.LevelDebug | -4 | подробности для разработчиков, в продакшене выключены |
slog.LevelInfo | 0 | обычные события: запуск, обслуженный запрос, завершённое задание |
slog.LevelWarn | 4 | что-то неожиданное, с чем программа справилась |
slog.LevelError | 8 | операция завершилась ошибкой |
Обработчик отбрасывает записи ниже своего минимального уровня, а минимум по умолчанию Info. Поэтому slog.Debug(...) ничего не печатает, пока вы не настроите обработчик с Level: slog.LevelDebug. Промежутки между значениями оставляют место для своих уровней вроде slog.Level(2).
Чтобы менять уровень во время работы (по флагу, через админский эндпоинт или по сигналу), положите в опции slog.LevelVar и позже вызывайте у него Set:
var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written
Текст или JSON
slog.NewTextHandler пишет пары key=value, которые легко читать в терминале. slog.NewJSONHandler пишет по одному JSON-объекту на строку, и именно такой формат ожидает большинство конвейеров логов. Вызовы логирования остаются теми же, меняется только обработчик.
Значения сохраняют свои типы: status в JSON-выводе это число, а retry булево значение, time.Duration печатается как 42ms в тексте и в наносекундах в JSON, а error печатает своё сообщение. ReplaceAttr это хук для переписывания или удаления атрибутов; здесь он убирает время, а на практике им переименовывают ключи (msg в message) или маскируют значения.
Атрибуты
Слаботипизированная форма чередует ключи и значения: "user", "ada", "attempts", 3. Она короткая, и у неё один вид сбоя: нечётное число аргументов. Лишнее значение логируется под ключом !BADKEY. go vet это ловит:
./main.go:14:2: call to slog.Info missing a final value
Для типобезопасности и чуть меньшего числа аллокаций используйте конструкторы атрибутов, а в горячем коде LogAttrs:
logger.Info("order placed",
slog.Int("order_id", 1017),
slog.String("currency", "EUR"),
slog.Float64("total", 59.90),
slog.Duration("took", elapsed),
)
logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))
Держите одно соглашение об именах ключей во всём коде (user_id везде, а не userID в одном пакете и uid в другом). От этого зависят запросы в вашей системе логов.
With: логгеры, которые несут контекст
logger.With(attrs...) возвращает новый логгер, который добавляет эти атрибуты к каждой записи. Создавайте такой на каждый запрос или задание, и все строки, которые он пишет, можно будет связать между собой:
Каждая строка несёт service, version, request_id и user, и повторять их в каждом вызове не нужно. slog.Group вкладывает атрибуты: JSON-обработчик пишет их вложенным объектом ("payment":{"amount":25,"currency":"USD"}), а текстовый ключами через точку (payment.amount=25). logger.WithGroup("db") помещает все последующие атрибуты этого логгера в группу.
Передавайте логгер уровня запроса вниз параметром или полем структуры. Хранить его в context.Context можно, но это прячет зависимость; методы slog вида InfoContext(ctx, ...) передают контекст обработчику, и собственный обработчик может достать из него trace ID.
Скрытие секретов через LogValuer
Тип может сам определять, как его логируют, реализовав slog.LogValuer. Так пароли и токены не попадают в логи, кто бы ни логировал значение:
User логирует только свой ID и email, а Token, залогированный отдельно, печатает REDACTED. Обработчик вызывает LogValue, только когда запись действительно пишется, так что это подходит и для значений, которые дорого вычислять.
Классический пакет log
log появился раньше slog и по-прежнему хорош для маленьких программ и скриптов. Он пишет строки в стандартный поток ошибок с префиксом из даты и времени:
| Флаг | Добавляет |
|---|---|
log.LstdFlags (по умолчанию) | дату и время 2009/11/10 23:00:00 |
log.Lmicroseconds | микросекунды ко времени |
log.LUTC | время в UTC |
log.Lshortfile / log.Llongfile | main.go:14 / полный путь |
log.Lmsgprefix | ставит префикс перед сообщением, а не в начало строки |
Три функции завершают программу или паникуют, и разница важна:
log.Fatal,log.Fatalf,log.Fatallnпечатают, а затем вызываютos.Exit(1). Отложенные вызовы не выполняются. Используйте их вmainдля сбоев при запуске и никогда в коде библиотек или обработчиках запросов.log.Panicи родственные печатают, а затем паникуют, так что отложенные вызовы выполняются, и панику можно перехватить.- Все остальные функции просто пишут строку.
Чтобы логировать в файл, откройте его и передайте в log.New или log.SetOutput; io.MultiWriter(os.Stderr, f) пишет в оба места.
log и slog вместе
slog.SetDefault(logger) делает logger логгером по умолчанию для функций верхнего уровня вроде slog.Info, а заодно направляет через него вывод пакета log. Существующие вызовы log.Printf в вашем коде или зависимостях тогда выходят структурированными записями уровня Info:
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}
До SetDefault логгер slog по умолчанию пишет через пакет log, поэтому голый slog.Info("hi") печатает 2026/09/23 14:30:00 INFO hi.
Практические правила
- Логируйте ошибку или возвращайте её, но не то и другое. Если функция логирует ошибку и возвращает её, один и тот же сбой логируется на каждом уровне стека вызовов. Возвращайте ошибки наверх с контекстом и логируйте один раз там, где их обрабатывают.
- Изменяемые данные кладите в атрибуты, а не в сообщение.
logger.Info("user created", "user_id", id)хорошо группируется в системе логов;logger.Info(fmt.Sprintf("user %d created", id))создаёт отдельное сообщение для каждого пользователя. - Никогда не логируйте секреты и полные тела запросов. Маскируйте через
LogValuerилиReplaceAttr. - JSON в продакшене, текст при разработке. Выбирайте обработчик при запуске по флагу или переменной окружения.
- Выбирайте уровни осознанно. Если всё логируется уровнем Error, оповещения об ошибках превращаются в шум.
Часто задаваемые вопросы
Что такое slog в Go?
log/slog это пакет структурированного логирования, добавленный в стандартную библиотеку в Go 1.21. Вместо форматированных строк у каждой записи есть сообщение, уровень (Debug, Info, Warn, Error) и атрибуты ключ-значение, а обработчик пишет её как текст key=value или как JSON: slog.Info("login", "user", "ada", "attempts", 3).
Как включить debug-логи в slog?
Минимальный уровень по умолчанию Info, поэтому slog.Debug ничего не печатает. Создайте обработчик с уровнем ниже и сделайте его логгером по умолчанию: slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))). Если уровень нужно менять во время работы программы, используйте slog.LevelVar вместо константы.
Чем log отличается от slog в Go?
log пишет строки в свободной форме с необязательным префиксом времени, и уровней у него нет. slog пишет записи с уровнями и типизированными атрибутами ключ-значение, которые сборщики логов умеют разбирать и фильтровать. Оба пакета в стандартной библиотеке; slog.SetDefault ещё и перенаправляет вывод пакета log через обработчик slog.
Выполняет ли log.Fatal отложенные функции?
Нет. log.Fatal и log.Fatalf печатают сообщение и вызывают os.Exit(1), который пропускает все отложенные вызовы. Используйте их только в main или коде настройки, где нечего очищать. log.Panic вместо этого паникует, так что отложенные вызовы выполняются.