---
title: "Дефолтный логгер: SetDefault и старый пакет log"
description: "Дефолтный логгер go: slog SetDefault, связь пакетов log и slog, что видят библиотеки и как одним вызовом поменять вывод всего сервиса и его библиотек."
course: "Логирование в Go: структурные логи на slog"
url: "https://golangmaster.ru/courses/slog/lessons/06-default-logger/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Дефолтный логгер: SetDefault и старый пакет log

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

Дефолтный логгер slog и вызов slog SetDefault — то место, где сходятся два мира: новый структурный API и старый пакет log, и от этой настройки зависит, куда и в каком виде попадут строки всего сервиса и его библиотек. Разберём маршрут записи по умолчанию, побочные эффекты SetDefault в обе стороны, момент, когда часть строк успевает уйти до настройки, и что остаётся у логгеров, созданных вручную через log.New.

У каждого процесса на slog есть настройка, которую вы не писали: дефолтный логгер. Пока в main не появился slog.New, все вызовы slog.Info, slog.Warn и slog.Error уходят в него, и это не заглушка, а маршрут через старый пакет log. Отсюда два практических вопроса, которые обычно всплывают при первой стычке с реальным сервисом. Первый: в каком виде выглядит строка, если ничего не настраивать. Второй: как одним движением переключить на свой формат весь сервис, включая код третьих библиотек, который вы не контролируете. Оба ответа сводятся к паре slog.SetDefault и связи пакетов log и slog: новый структурный API пишет через дефолтный логгер, а после SetDefault и старый log пишет через ваш обработчик. В уроке разберём формат по умолчанию и его уровень, механику SetDefault с побочными эффектами, строки, которые успевают уйти до настройки, и мосты для логгеров из log.New.

### Дефолтный логгер go: формат и уровень

Разбор дефолтного логгер go стоит начать с того, что видит терминал при первом же запуске. Вызов slog.Info("кэш прогрет", "entries", 512) на чистом пакете печатает строку вида 2009/11/10 23:00:00 INFO кэш прогрет entries=512. Разбор по частям: дата и время — это флаги пакета log, через который дефолтный обработчик выводит запись; INFO — уровень; дальше сообщение, а за ним атрибуты в стиле text-формата из пятого урока, ключ равно значение. По умолчанию дефолтный обработчик пропускает записи уровня Info и выше: Debug на нём невидим. Это меняет функция slog.SetLogLoggerLevel, появившаяся в Go 1.22: передайте ей slog.LevelDebug, и отладочные записи начнут печататься без единой строки своего кода. Маршрут у записи простой: slog.Info собирает запись и отдаёт её обработчику по умолчанию, тот форматирует строку и отправляет в пакет log, а пакет пишет её в стандартный поток ошибок. Поэтому дежурному, который ищет строки сервиса до настройки, нужен именно stderr, а не stdout — собственный обработчик в main потом перенесёт вывод куда положено.

### slog SetDefault: один вызов на весь сервис

Функция slog SetDefault принимает логгер и делает его общим для пакета: после вызова все slog.Info, slog.Warn, slog.Error и slog.Debug без приёмника идут через обработчик этого логгера. Но у вызова есть побочный эффект, о котором в документации одна строка, а на практике — половина пользы: SetDefault переподключает и пакет log. Если обработчик нового логгера не внутренний дефолтный, функция ставит пакету log адаптер, который превращает обычные строки log.Printf в структурные записи на уровне Info, и сбрасывает флаги пакета в ноль, чтобы дата и имя файла не задваивались со служебными полями обработчика. Итог: легаси-код и библиотеки, которые пишут через log, попадают в ваш JSON или текстовый формат без правки их исходников. Это самый дешёвый способ собрать разнородные строки сервиса в один поток. Обратная сторона: уровень у таких строк один — Info, а текст сообщения становится полем msg целиком, вместе с уже раскрытыми Printf-ом глаголами. Разбирать такой JSON сложнее, чем записи с настоящими атрибутами, поэтому мост для log-строк — мера на время миграции, а не образ жизни.

### SetDefault на старте main

```go
package main

import (
        "log"
        "log/slog"
        "os"
)

func main() {
        logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
        slog.SetDefault(logger)

        slog.Info("порт открыт", "port", 8080)
        log.Println("легаси-строка из старого кода")
}
```

Две строки настройки — и обе записи приедут в stdout одним JSON-объектом: slog.Info с атрибутом port, log.Println текстом в поле msg на уровне INFO. Даты и префиксы пакета log исчезли: SetDefault обнулил флаги, чтобы служебные поля не дублировались с time и level. Замените NewJSONHandler на NewTextHandler — формат сменится без других правок.

### Куда уходит строка при каждом вызове

| Вызов | До SetDefault | После SetDefault с JSONHandler |
| --- | --- | --- |
| slog.Info | stderr, текстовый формат дефолтного обработчика | ваш обработчик: JSON в stdout |
| slog.Debug | скрыт: минимум дефолтного обработчика — Info | решает уровень вашего обработчика |
| log.Println | stderr, формат пакета log с датой | ваш обработчик, уровень Info, текст в msg |
| свой логгер из log.New | его собственный writer | тот же writer: SetDefault его не трогает |

### Что видят библиотеки и когда они это видят

Главный вопрос интеграции: что видят библиотеки, чей код вы не правили. Ответ зависит от того, как они логируют. Библиотека, использующая slog.Info напрямую, при каждом вызове берёт текущий дефолтный логгер — значит, после SetDefault её строки окажутся в вашем формате с полной структурой. Библиотека на log.Printf после SetDefault тоже попадает в ваш обработчик, но текстом на уровне Info. А вот библиотека, которая при старте сохранила собственный логгер, созданный через log.New с отдельным writer, продолжает писать в свой поток: SetDefault меняет логгер пакета, а не чужие экземпляры. Второй важный нюанс — момент. SetDefault работает на будущее: всё, что успело напечататься до вызова, уже ушло в старый вывод и не перепрочитается. Импортируемые пакеты выполняют свои init-функции до main, и если библиотека логирует на инициализации, эти строки пройдут мимо настройки. Отсюда правило, которое стоит сделать рефлексом: SetDefault — первая содержательная строка main, до чтения конфига, до запуска фоновых горутин и до конструирования клиентов, которые могут захотеть залогировать подключение.

> **SetDefault в конце main: ночная половина лога мимо.** Кейс: в сервис добавили структурные логи, а вызов SetDefault поставили после инициализации клиента очереди. Клиент при конструировании проверяет соединение и пишет предупреждение через log.Printf. Ночью очередь мигнула, предупреждение ушло в stderr старым текстовым форматом, а дежурный разбирал JSON-поток: grep по level=warn ничего не дал, потому что у записи уровня не было вовсе. Разбор занял больше времени, чем сама миграция. Лечение простое: SetDefault в начале main, до конструирования любых клиентов и до go-рутин, а обёртки вида log.New с отдельным файлом выписать списком и переосмыслить при миграции.

### Уровень дефолтного обработчика

```go
// Go 1.22 и новее: поднять минимум у дефолтного обработчика
slog.SetLogLoggerLevel(slog.LevelDebug)
slog.Debug("соединение переподключено", "attempt", 3)
```

До этой строки Debug на дефолтном логгере отбрасывался: минимум стоял на Info. SetLogLoggerLevel меняет и порог дефолтного обработчика, и уровень, на котором пакет log пишет после моста. Полезно на отладке локального стенда, когда свой логгер ещё не собран, а Debug-вывод чужой библиотеки нужен целиком.

### log и slog после SetDefault: что теряется

Мост из log в slog удобен, но у него есть цена, и лучше знать её заранее. Теряются: префиксы, которые задавал log.SetPrefix, флаги с датой, файлом и строкой вызова и уровень — его у текстовой строки нет, поэтому всё пишется как Info. Сохраняются: текст сообщения с раскрытыми глаголами форматирования и порядок строк — записи не перемешаются, потому что пакет log сериализует вывод мьютексом, как и до моста. Отдельно про обёртки: если в проекте есть хелпер, который зовёт log.Output(2, msg) ради правильного имени файла в выводе, при переходе на slog вычислять глубину вызова не нужно — Handler сам достанет место вызова через AddSource из двенадцатого урока. А если сервис держит отдельные log.Logger для разных назначений, их мостит slog.NewLogLogger — тема десятого урока; здесь достаточно помнить, что SetDefault перенаправляет только логгер пакета, то есть log.Default().

> **Код урока — локально, интерактивы — на странице.** В браузерной песочнице доступны fmt, strings, strconv, math, sort, errors и time, пакета log/slog там нет — механику формата честно разминают на fmt.Fprintln(os.Stderr, ...) и time.Now().Format: соберите теми же средствами строку вида время LEVEL сообщение key=value. Полные slog.SetDefault и мост из log запускаются локально через go run; интерактивы ниже проверят порядок настройки, формат дефолтного вывода и маршрут log-строк.

### Правила дефолтного логгера

- SetDefault — первая содержательная строка main: всё, что напечатано до неё, уходит в старый вывод.
- После SetDefault строки пакета log идут через ваш обработчик на уровне Info с обнулёнными флагами.
- Свои логгеры из log.New SetDefault не трогает — для них есть slog.NewLogLogger из десятого урока.
- Минимальный уровень дефолтного обработчика меняет slog.SetLogLoggerLevel (Go 1.22).
- До настройки вывод идёт в стандартный поток ошибок: ищите строки сервиса в stderr, а не в stdout.
- Мост для log-строк — мера на время миграции: текст в msg проигрывает настоящим атрибутам.

Три вопроса, которые чаще всего всплывают на ревью такой настройки. Первый: «не потеряем ли строки, если SetDefault сработает неудачно?» — вызов не возвращает ошибку и не паникует: он назначает логгер, а сам логгер уже собран и проверен New-фабрикой. Второй: «что будет, если writer закрыт?» — обработчик пишет в переданный writer, и ошибка записи возвращается туда, куда её отдаёт Writer; это повод не строить молчаливые обёртки, а оставить обычный os.Stdout и собрать вывод на стороне окружения. Третий: «зачем мост, если легаси перепишем через месяц?» — потому что месяц переезда на живом сервисе выглядит как недели смешанного вывода, и без моста половина инцидентов разбиралась бы по двум файлам с разными форматами. На такие вопросы отвечает и таблица выше: маршрут каждой строки зависит от того, кем она написана и успел ли сработать SetDefault.

Связи с другими уроками курса: [первый вывод slog.Info](/courses/slog/lessons/02-pervye-stroki-loga/) показывал формат по умолчанию вживую, [пятый урок](/courses/slog/lessons/05-text-vs-json/) разбирает форматы, которые SetDefault подставляет всему сервису. Куда писать после настройки — вопрос [шестнадцатого урока про stdout и файлы](/courses/slog/lessons/16-kuda-pishut-logi/), а мосты для старого пакета log разворачивает [десятый урок про миграцию](/courses/slog/lessons/10-migraciya-s-log/). Если структурные логи понадобились, не дожидаясь конца миграции, посмотрите [курс по zap](/courses/zap/) — там та же схема полей и уровней с другим обработчиком. Карта курса — на [странице курса](/courses/slog/).

### Проверка: мост для пакета log

**Вопрос:** После slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil))) легаси-код вызывает log.Println("готово"). Что окажется в выводе?

- ✅ JSON-запись уровня INFO с текстом «готово» в поле msg
- Текстовая строка с датой в исходном формате пакета log
- Ничего: пакет log не знает про slog и пишет в никуда
- JSON-запись уровня ERROR: строки пакета log повышают уровень

SetDefault подключает к пакету log адаптер, который пишет через ваш обработчик на уровне Info, текст целиком уходит в msg. Формат пакета log остался бы только без SetDefault — на исходном stderr с датой.

### Проверка: Debug на дефолтном логгере

**Вопрос:** Сервис ещё без своего обработчика, а Debug-записи нужны на локальном стенде. Что сделать?

- ✅ Вызвать slog.SetLogLoggerLevel(slog.LevelDebug) — Go 1.22 и новее
- Вызвать log.SetLevel(slog.LevelDebug): у пакета log есть такой метод
- Вызвать slog.SetDefault(nil): без логгера пишутся все уровни
- Ничего: на дефолтном логгере Debug выключен и не включается

SetLogLoggerLevel поднимает минимум дефолтного обработчика, и Debug начинает печататься. У пакета log метода SetLevel нет, а SetDefault(nil) — паника на первом же вызове записи.

### Что напечатает программа?

```go
func main() {
        slog.Info("кэш прогрет", "entries", 512)
}
```

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

- ✅ 2009/11/10 23:00:00 INFO кэш прогрет entries=512 — дата от пакета log, атрибуты key=value
- Объект JSON с полями time, level, msg, entries — JSON используется по умолчанию
- кэш прогрет entries=512 — без служебных полей
- INFO кэш прогрет — без даты и атрибутов

Без настройки запись уходит через дефолтный обработчик в пакет log: дата и время — флаги пакета, уровень INFO, дальше сообщение и атрибуты в текстовом стиле. JSON появится только после SetDefault с JSONHandler.

### Соберите настройку на старте

Допишите: назначить логгер дефолтным, сделать структурную запись и печать из старого пакета.

```go
func main() {
        logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
        slog.___(logger)

        slog.___("порт открыт", "port", 8080)
        log.___("легаси-строка из старого кода")
}
```

Подсказка: Общий логгер назначает SetDefault, структурную запись делает Info, а строку старого пакета печатает Println.

Правильные ответы в пропусках: `SetDefault`, `Info`, `Println`

### Порядок настройки в main

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

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

1. `func main() {`
2. `logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))`
3. `slog.SetDefault(logger)`
4. `slog.Info("сервис готов принимать трафик", "port", 8080)`
5. `log.Println("легаси-библиотека пишет через пакет log")`
6. `}`

### Карточки: дефолтный логгер

- **slog.SetDefault** — Назначает логгер общим для пакета: slog.Info без приёмника и строки пакета log после вызова идут через его обработчик.
- **дефолтный обработчик** — Встроенный маршрут slog до настройки: текстовый формат с датой через пакет log, минимум Info, вывод в stderr.
- **slog.SetLogLoggerLevel** — Меняет минимальный уровень дефолтного обработчика и уровень log-строк после моста. Появилась в Go 1.22.
- **log после SetDefault** — Пишет на уровне Info с обнулёнными флагами, текст целиком в msg; префиксы, дата и место вызова теряются.
- **log.New(...)** — Создаёт независимый логгер со своим writer: SetDefault его не перенаправляет, мостом служит slog.NewLogLogger.

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

- Без настройки slog пишет через дефолтный обработчик: текстовый формат с датой, минимум Info, вывод в stderr.
- slog.SetDefault делает логгер общим и перенаправляет пакет log в его обработчик, сбрасывая флаги.
- log-строки после моста пишутся на уровне Info текстом в msg — это мера на время миграции.
- Свои логгеры из log.New SetDefault не трогает — для них нужен slog.NewLogLogger.
- Минимальный уровень дефолтного обработчика поднимает slog.SetLogLoggerLevel (Go 1.22).
- SetDefault ставят первой содержательной строкой main: init-код и ранние горутины успевают написать до неё.

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

Запустите программу урока локально, затем поменяйте NewJSONHandler на NewTextHandler и убедитесь, что и структурная строка, и легаси-строка сменили формат без других правок. После этого вызовите SetLogLoggerLevel с LevelDebug, добавьте в main пару slog.Debug и проверьте, какие из записей видны, а какие остались скрытыми.

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

### Нужно ли вызывать SetDefault, если slog используется только в своём коде?

Тогда логгер можно держать в main и передавать по зависимостям — это явнее глобального состояния. SetDefault нужен, когда хочется повлиять на библиотеки: и на те, что зовут slog.Info напрямую, и на те, что пишут через пакет log.

### Что произойдёт, если вызвать SetDefault дважды?

Действовать будет последний вызов: и slog.Info без приёмника, и пакет log пойдут через обработчик нового логгера. В тестах этим пользуются — подменяют дефолтный логгер и восстанавливают его через defer, в проде настройку делают один раз в main.

### Куда попадают записи после SetDefault с JSONHandler(os.Stdout)?

В стандартный поток вывода процесса, а дальше — куда он ведёт: в Docker-журнал, коллектор или терминал. Сам пакет log до моста писал в stderr, поэтому на переходном этапе строки могут оказаться в двух потоках — ещё один аргумент настроить всё сразу.
