Типичный день: вы пишете первый HTTP-обработчик и хотите видеть в консоли, что происходит внутри. fmt.Println справляется, но уже на второй день строк становится десятки, все одного веса, и отличить обычный ход работы от проблемы можно только прочитав каждую. Здесь появляется slog.Info — вызов, которому передают сообщение и поля отдельно, а он сам собирает запись с уровнем и временем. В этом уроке разберём анатомию вызова и формат вывода по умолчанию: что печатается, куда печатается и почему выглядит именно так. Затем проверим себя из терминала — перенаправлениями и флагами пакета log — и честно поговорим о том, что этот формат рабочий инструмент разработки, а не контракт для продакшена.
Slog Info: анатомия вызова#
Сигнатура выглядит скромно: func Info(msg string, args ...any) — сообщение и любое число аргументов. Аргументы читаются парами: строка-ключ, затем значение, — и так до конца списка. Вызов slog.Info("заказ оформлен", "id", 42, "amount", 990) означает: записать событие уровня Info с сообщением «заказ оформлен» и двумя полями. По той же форме работают slog.Debug, slog.Warn и slog.Error — разница только в уровне, который попадёт в запись. Сам вызов ничего не форматирует: он собирает Record — структуру с временем, уровнем, сообщением и атрибутами — и передаёт её обработчику, а тот решает, во что превратить запись. Эта развязка «вызов — обработчик» и есть главная идея slog: код логирования не меняется, когда вы меняете формат вывода. У обработчика по умолчанию формат неожиданно простой, и его стоит увидеть глазами — этим займёмся прямо сейчас.
Минимальная программа целиком
GOpackage main
import (
"log/slog"
)
func main() {
slog.Info("сервис запущен", "port", 8080)
}Четыре значимые строки — и запись уже в консоли. На экране появится строка вида «2025/03/14 09:30:00 INFO сервис запущен port=8080». Дата и время в начале — не заслуга slog: обработчик по умолчанию пишет через пакет log, а тот добавляет свой префикс. INFO — уровень записи, дальше сообщение и пары ключ=значение в порядке вызова. Если запустить программу и не увидеть строку — смотрите stderr: в терминале оба потока смешаны, но конвейеры их различают. Разбор старого пакета log и моста в slog — тема десятого урока.
Теперь разложим строку по частям, потому что в ней смешаны три источника. Префикс с датой и временем даёт пакет log с его стандартными флагами — slog к нему отношения не имеет. Слово INFO ставит сам slog: это уровень записи, напечатанный заглавными буквами. Всё остальное — сообщение и атрибуты — рендерит обработчик: сообщение печатается как есть, каждая пара выглядит как ключ=значение и отделяется пробелом. Значения форматируются так, как fmt печатает любое значение по умолчанию: число остаётся числом без кавычек, строка — строкой, слайс разворачивается в квадратные скобки. Важно, что порядок пар — это порядок аргументов в вызове: обработчик ничего не сортирует и не переименует. Отсюда практическое правило: самые важные для поиска поля ставьте первыми, чтобы в длинной строке их было видно без прокрутки. Полная карта частей — в таблице ниже.
Части строки по умолчанию#
| Часть | Кто её поставил | Пример |
|---|---|---|
| Дата и время | пакет log со стандартными флагами | 2025/03/14 09:30:00 |
| Уровень | slog по вызову Info, Warn, Error | INFO |
| Сообщение | первый аргумент вызова | сервис запущен |
| Атрибуты | пары ключ-значение из args | port=8080 |
| Поток | stderr у дефолтного обработчика | виден в docker logs и journalctl |
Кейс: команда запустила сервис через ./app > app.log и не нашла в файле ни одной строки лога — записи ушли в stderr и пропали мимо файла. Обработчик по умолчанию пишет в стандартный поток ошибок: это осознанное решение, чтобы логи не смешивались с полезным выводом программы. Фикс одной строкой — ./app > out.log 2> app.log — и каждая запись на месте. В Docker и journald оба потока собираются вместе, поэтому в контейнере разница незаметна, а на локальной машине — заметна.
Slog пример с разными типами значений#
Пары принимают any, поэтому в атрибуты попадают числа, булевы, слайсы и ошибки. Смотри slog пример ниже: значения печатаются так, как fmt рисует их по умолчанию, — и это удобно для человека, но у подхода есть граница, о которой стоит знать заранее. А ещё пары умеют ошибаться: нечётное число аргументов или нестроковый ключ не роняют программу — вместо этого появляется поле с служебным именем !BADKEY. Программа продолжает работать, запись печатается, но поле получает имя-заглушку, и поиск по настоящему ключу в хранилище его больше не найдёт. Это самая тихая из ошибок логирования: компилятор молчит, тесты зелёные, а поля в логе нет. Типизированные конструкторы атрибутов — slog.Int, slog.String и остальные — закрывают часть таких ситуаций на этапе компиляции, их подробно разбирает четвёртый урок.
Типы значений и ловушка !BADKEY
GOslog.Info("заказ оформлен", "id", 42, "ok", true, "items", []string{"coffee", "tea"})
// нечётное число аргументов: последнему не хватило ключа
slog.Info("платёж завис", "id", 42, "urgent")Первая запись напечатается так: INFO заказ оформлен id=42 ok=true items=[coffee tea] — слайс развернулся в скобках, булево стало true, всё без кавычек. Вторая запись покажет сюрприз: INFO платёж завис id=42 !BADKEY=urgent. Значение urgent есть, ключа нет — slog честно сообщил об этом служебным именем, а не паникой. На проде такая строка ищется по слову urgent в теле сообщения, а не по полю, и именно поэтому нечётные пары разыскивают код-ревью, а не оставляют на удачу.
Дефолтный вид хорош для разработки, но он собран на пакете log, а значит поддаётся его настройке: log.SetFlags(0) убирает дату и время, log.SetOutput перенаправляет поток. Настоящая смена формата — замена обработчика: TextHandler печатает без префикса пакета log, JSONHandler отдаёт машинные строки, и оба разбираются в пятом уроке. Точка замены глобального логгера — slog.SetDefault, её показывает шестой урок.
Лог из песочницы и из терминала#
Честно про инструменты этого курса: в песочнице сайта пакета log/slog нет — там работает подмножество стандартной библиотеки, о котором скажем ниже в совете. Поэтому slog info вывод проверяется из терминала, и проверка занимает полминуты. Создайте папку, выполните go mod init example/logdemo, положите файл main.go с программой из урока и запустите go run main.go — строка появится сразу. Теперь два эксперимента, которые закрепляют понимание потоков: go run main.go 2> app.log покажет пустой экран и непустой файл, потому что записи ушли в stderr; конвейер go run main.go 2>&1 | grep INFO вернёт строку обратно в смешанный поток и отфильтрует по уровню. На живом сервисе те же проверки делает окружение: docker logs показывает оба потока вместе, а двадцать четвёртый урок разбирает сбор полностью. Механику же форматирования полезно прогнать руками в песочнице — fmt печатает значения так же, как дефолтный обработчик slog.
Шаги проверки из терминала#
- go mod init example/logdemo — инициализация модуля, чтобы go run работал без сюрпризов.
- Сохраните main.go с программой из урока: импорт log/slog и один вызов slog.Info.
- go run main.go — строка с датой, INFO, сообщением и полями появляется на экране.
- ./app 2> app.log (после go build) — stdout пуст, все записи в файле ошибок.
- go run main.go 2>&1 | grep INFO — проверка фильтра по уровню в конвейере.
- log.SetFlags(0) в коде — строка без даты: видно, что префикс давал пакет log.
Спорное мнение: дефолтного формата хватает до продакшена#
Мнение для обсуждения: менять формат вывода в первые недели проекта — преждевременная оптимизация. Дефолтный вид читается глазами, требует нуля строк конфигурации и честно показывает уровни и поля; TextHandler добавит разве что поле time в другом формате. Против этого два честных аргумента: если логи с первого дня уезжают в хранилище, парсить префикс пакета log коллектору неприятно; если команда привыкла к JSON в консоли, возврат к тексту раздражает. Граница, по моему опыту, такая: пока проект читает логи сам в терминале — сидите на дефолте; как только появился первый внешний потребитель записей — ставьте JSONHandler одной строкой и не носите с собой совместимость. Ошибка противоположного рода — настраивать структуру поля заранее, до первого запроса дежурного: реальный список нужных полей появляется после первого же разбора, а не из фантазии на архитектурной доске.
В песочнице сайта нет log/slog — только подмножество стандартной библиотеки: fmt, strings, strconv, math, sort, errors, time. Но механику дефолтного вывода она честно разминает: соберите fmt.Println("INFO", "сервис запущен", "port=8080") и сравните со строкой из терминала — совпадёт почти дословно, кроме даты и потока. Дальше можно имитировать и !BADKEY: выведите пару, у которой потеряли ключ, и посмотрите, как запись теряет смысл без поля. Полный slog запускайте локально — go run из шагов выше.
Проверка: поток по умолчанию
Куда обработчик по умолчанию пишет записи slog.Info?
Проверка: нечётное число аргументов
Что напечатает slog.Info("платёж завис", "id", 42, "urgent")?
Что выведет программа?
Прочитайте код и выберите, что он выведет:
func main() {
log.SetFlags(0)
slog.Info("готово", "id", 7)
}Соберите проверку формата
Допишите отключение префикса, вызов записи и значение атрибута.
Соберите программу с двумя записями
Расставьте строки в порядке выполнения: пакет, импорт, точка входа, обычная запись, предупреждение, закрывающая скобка.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: slog.Info и вывод по умолчанию
Первые строки написаны и разобраны до символа: вызов собирает запись, обработчик по умолчанию рисует её через пакет log в stderr, пары ключ-значение сохраняют порядок вызова, а потерянный ключ честно зовётся !BADKEY. Дальше по спирали: в третьем уроке разберём уровни и решим, что писать на Debug, а что на Error; в четвёртом — типизированные конструкторы атрибутов, которые выключают целый класс ошибок с парами; в пятом — настоящую смену формата через TextHandler и JSONHandler. Карта курса — на его странице, домашнее задание ниже закрепляет поток stderr руками.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- slog.Info(msg, args...) читает аргументы парами ключ-значение и передаёт запись обработчику, не форматируя ничего сам.
- Строка по умолчанию: дата и время из пакета log, уровень INFO, сообщение, пары ключ=значение в порядке вызова.
- Дефолтный обработчик пишет в stderr — ./app > out.log оставляет логи мимо файла, нужен 2>.
- Значения печатаются как fmt: числа без кавычек, слайсы в скобках; поля не сортируются.
- Нечётное число аргументов даёт !BADKEY вместо паники: ловить в ревью, а не на проде.
- log.SetFlags и log.SetOutput настраивают префикс и поток; смену формата делает замена обработчика.
Домашнее задание
Соберите программу из урока и прогоните три проверки: go run main.go с выводом на экран, запуск после go build с перенаправлением 2> app.log и конвейер 2>&1 | grep INFO. Затем добавьте в код log.SetFlags(0) и второй вызов с нечётным числом аргументов — найдите !BADKEY в своей же записи и перепишите вызов правильными парами.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему в строке slog дата и время в формате пакета log?
Обработчик по умолчанию пишет через пакет log, а тот добавляет префикс по своим флагам. Смените его через log.SetFlags(0) — префикс исчезнет; поставьте TextHandler или JSONHandler — дата станет полем time в собственном формате slog.
Можно ли смешивать строковые пары и slog.Attr в одном вызове?
Да: аргументы вида ключ, значение и готовые slog.Int("id", 42) соседствуют в одном вызове, порядок сохраняется. Смешение удобно при переходе — но в чистом коде команды обычно договариваются об одном стиле, и Attr выигрывает проверкой типов.
Как напечатать структуру целиком?
Передайте её значением: slog.Info("заказ", "order", order) — дефолтный обработчик отрисует поля через fmt. Для машинного вывода это скверно: структура развалится в текст, а не в поля JSON. Разложите нужные поля атрибутами или примените slog.Any с LogValuer — об этом девятый урок.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
PostgreSQL в Go: драйвер pgx с нуля до продакшена
Курс pgx для Go: подключение к PostgreSQL, pgxpool, сканирование структур, транзакции, Batch, COPY, JSONB, LISTEN/NOTIFY и тесты. 25 уроков.
- Подключаться к PostgreSQL через pgx и pgxpool
- Сканировать строки в структуры и справляться с NULL
- Работать с типами Postgres: UUID, JSONB, массивы, timestamptz
- Проводить транзакции и массовые вставки через Batch и COPY