---
title: "Первые строки: slog.Info и вывод по умолчанию"
description: "Slog Info в Go: вызов slog.Info, формат вывода по умолчанию, пары ключ-значение, stderr и проверка лога из терминала. Slog пример с разбором каждой части."
course: "Логирование в Go: структурные логи на slog"
url: "https://golangmaster.ru/courses/slog/lessons/02-pervye-stroki-loga/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Первые строки: slog.Info и вывод по умолчанию

*Курс «Логирование в Go: структурные логи на slog», урок 2 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/slog/lessons/02-pervye-stroki-loga/*

Первый slog пример обычно вызывают до чтения документации — и это правильно: slog.Info мгновенно даёт строку с уровнем, сообщением и полями. Но строка по умолчанию собрана из трёх источников, и её стоит разобрать на части: что даёт пакет log, откуда берётся уровень INFO и почему пары ключ-значение печатаются именно в этом порядке. Разберём вывод до символа, найдём stderr и соберём проверку из терминала.

Типичный день: вы пишете первый 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: код логирования не меняется, когда вы меняете формат вывода. У обработчика по умолчанию формат неожиданно простой, и его стоит увидеть глазами — этим займёмся прямо сейчас.

### Минимальная программа целиком

```go
package 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 — тема [десятого урока](/courses/slog/lessons/10-migraciya-s-log/).

Теперь разложим строку по частям, потому что в ней смешаны три источника. Префикс с датой и временем даёт пакет log с его стандартными флагами — slog к нему отношения не имеет. Слово INFO ставит сам slog: это уровень записи, напечатанный заглавными буквами. Всё остальное — сообщение и атрибуты — рендерит обработчик: сообщение печатается как есть, каждая пара выглядит как ключ=значение и отделяется пробелом. Значения форматируются так, как fmt печатает любое значение по умолчанию: число остаётся числом без кавычек, строка — строкой, слайс разворачивается в квадратные скобки. Важно, что порядок пар — это порядок аргументов в вызове: обработчик ничего не сортирует и не переименует. Отсюда практическое правило: самые важные для поиска поля ставьте первыми, чтобы в длинной строке их было видно без прокрутки. Полная карта частей — в таблице ниже.

### Части строки по умолчанию

| Часть | Кто её поставил | Пример |
| --- | --- | --- |
| Дата и время | пакет log со стандартными флагами | 2025/03/14 09:30:00 |
| Уровень | slog по вызову Info, Warn, Error | INFO |
| Сообщение | первый аргумент вызова | сервис запущен |
| Атрибуты | пары ключ-значение из args | port=8080 |
| Поток | stderr у дефолтного обработчика | виден в docker logs и journalctl |

> **Записи уходят в stderr, а не в stdout.** Кейс: команда запустила сервис через ./app > app.log и не нашла в файле ни одной строки лога — записи ушли в stderr и пропали мимо файла. Обработчик по умолчанию пишет в стандартный поток ошибок: это осознанное решение, чтобы логи не смешивались с полезным выводом программы. Фикс одной строкой — ./app > out.log 2> app.log — и каждая запись на месте. В Docker и journald оба потока собираются вместе, поэтому в контейнере разница незаметна, а на локальной машине — заметна.

### Slog пример с разными типами значений

Пары принимают any, поэтому в атрибуты попадают числа, булевы, слайсы и ошибки. Смотри slog пример ниже: значения печатаются так, как fmt рисует их по умолчанию, — и это удобно для человека, но у подхода есть граница, о которой стоит знать заранее. А ещё пары умеют ошибаться: нечётное число аргументов или нестроковый ключ не роняют программу — вместо этого появляется поле с служебным именем !BADKEY. Программа продолжает работать, запись печатается, но поле получает имя-заглушку, и поиск по настоящему ключу в хранилище его больше не найдёт. Это самая тихая из ошибок логирования: компилятор молчит, тесты зелёные, а поля в логе нет. Типизированные конструкторы атрибутов — slog.Int, slog.String и остальные — закрывают часть таких ситуаций на этапе компиляции, их подробно разбирает [четвёртый урок](/courses/slog/lessons/04-klyuchi-atributy/).

### Типы значений и ловушка !BADKEY

```go
slog.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 отдаёт машинные строки, и оба разбираются в [пятом уроке](/courses/slog/lessons/05-text-vs-json/). Точка замены глобального логгера — slog.SetDefault, её показывает [шестой урок](/courses/slog/lessons/06-default-logger/).

### Лог из песочницы и из терминала

Честно про инструменты этого курса: в песочнице сайта пакета 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 показывает оба потока вместе, а [двадцать четвёртый урок](/courses/slog/lessons/24-logi-na-prode/) разбирает сбор полностью. Механику же форматирования полезно прогнать руками в песочнице — fmt печатает значения так же, как дефолтный обработчик slog.

### Шаги проверки из терминала

1. go mod init example/logdemo — инициализация модуля, чтобы go run работал без сюрпризов.
2. Сохраните main.go с программой из урока: импорт log/slog и один вызов slog.Info.
3. go run main.go — строка с датой, INFO, сообщением и полями появляется на экране.
4. ./app 2> app.log (после go build) — stdout пуст, все записи в файле ошибок.
5. go run main.go 2>&1 | grep INFO — проверка фильтра по уровню в конвейере.
6. 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?

- ✅ В stderr — стандартный поток ошибок
- В stdout — стандартный поток вывода
- В файл app.log в текущей папке
- Никуда, пока не вызван slog.SetDefault

Дефолтный обработчик пишет через пакет log, а тот направляет вывод в stderr. Поэтому ./app > out.log оставляет логи за бортом файла — их ловит перенаправление 2>. В Docker и journald оба потока собираются вместе, и там разница не видна.

### Проверка: нечётное число аргументов

**Вопрос:** Что напечатает slog.Info("платёж завис", "id", 42, "urgent")?

- ✅ Запись с полем !BADKEY=urgent — последнему значению не хватило ключа
- Панику из-за нечётного числа аргументов
- Ошибку компиляции: пары должны быть чётными
- Запись без значения: лишний аргумент молча отбросится

Аргументы читаются парами ключ-значение; когда пары не сходятся, slog не паникует и не молчит — он делает атрибут с ключом !BADKEY. Запись печатается, программа работает, но поля с настоящим именем в логе нет. Потому нечётные пары ловят в ревью, а не в рантайме.

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

```go
func main() {
        log.SetFlags(0)
        slog.Info("готово", "id", 7)
}
```

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

- ✅ INFO готово id=7 — без даты и времени
- 2025/03/14 09:30:00 INFO готово id=7
- {"level":"INFO","msg":"готово","id":7}
- Ничего: SetFlags отключил и запись тоже

Дату и время в дефолтной строке ставил пакет log со своими флагами, а не slog. log.SetFlags(0) снимает префикс, и остаётся чистая запись: уровень, сообщение, поля. Сама запись от этого не пострадала — флаги управляют только внешним видом префикса, а SetOutput при необходимости меняет направление потока.

### Соберите проверку формата

Допишите отключение префикса, вызов записи и значение атрибута.

```go
func main() {
        log.___(0)
        slog.___("готово", "id", ___)
}
```

Подсказка: Префикс пакета log снимает SetFlags, запись уровня Info делает slog.Info, а идентификатор в примере — 7.

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

### Соберите программу с двумя записями

Расставьте строки в порядке выполнения: пакет, импорт, точка входа, обычная запись, предупреждение, закрывающая скобка.

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

1. `package main`
2. `import "log/slog"`
3. `func main() {`
4. `slog.Info("запрос обработан", "path", "/health")`
5. `slog.Warn("медленный запрос", "path", "/pay", "ms", 812)`
6. `}`

### Карточки: slog.Info и вывод по умолчанию

- **slog.Info** — Запись уровня Info: сообщение и пары ключ-значение. По той же форме работают Debug, Warn и Error — разница в уровне.
- **Формат по умолчанию** — Дата и время из пакета log, уровень INFO, сообщение, пары ключ=значение в порядке вызова. Значения печатает fmt.
- **stderr** — Поток по умолчанию для записей slog. Ловится перенаправлением 2>, в Docker и journald собирается вместе с stdout.
- **!BADKEY** — Служебный ключ, который slog даёт значению без пары: нечётное число аргументов или нестроковый ключ. Программа не падает.
- **log.SetFlags(0)** — Убирает префикс с датой и временем: его ставил пакет log, а не slog. Показывает, где граница между log и slog.
- **Порядок атрибутов** — Пары печатаются в порядке передачи аргументов: обработчик не сортирует и не переименовывает. Важные поля ставят первыми.

Первые строки написаны и разобраны до символа: вызов собирает запись, обработчик по умолчанию рисует её через пакет log в stderr, пары ключ-значение сохраняют порядок вызова, а потерянный ключ честно зовётся !BADKEY. Дальше по спирали: в [третьем уроке](/courses/slog/lessons/03-urovni-logov/) разберём уровни и решим, что писать на Debug, а что на Error; в [четвёртом](/courses/slog/lessons/04-klyuchi-atributy/) — типизированные конструкторы атрибутов, которые выключают целый класс ошибок с парами; в [пятом](/courses/slog/lessons/05-text-vs-json/) — настоящую смену формата через TextHandler и JSONHandler. Карта курса — на [его странице](/courses/slog/), домашнее задание ниже закрепляет поток stderr руками.

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

- 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 — об этом девятый урок.
