Содержание урока · 12 разделов
- Установка: go mod init и go get
- zap.NewProduction: читаем JSON по ключам
- Ключи записи по умолчанию в production
- NewDevelopment: формат для человека
- Какой пресет выбрать: окружение решает
- Логгер — иммутабельная структура
- Три ошибки первой недели
- Сообщение и поля: что куда класть
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Установка: go mod init и go get#
Начните с пустого модуля: mkdir demo, cd demo, затем go mod init demo. Дальше — go get go.uber.org/zap: команда подтянет последний релиз ветки v1 и пропишет его в go.mod. На этом установка закончена — zap не требует инициализации, конфигов и внешних сервисов, логгер создаётся одним вызовом конструктора. Одна оговорка о формате курса: песочница сайта исполняет только чистый Go без сторонних модулей, поэтому zap-код здесь не запускается — держите терминал под рукой. Понимание проверяют quiz, predict, fill и order-задачи, а запускать примеры вы будете локально, где zap честно печатает свои строки в stdout. Это, кстати, лучший способ учиться: разница между двумя пресетами, о которой мы будем говорить, видна глазами за секунду.
Первая программа с логгером
GOpackage main
import (
"errors"
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
logger.Info("service started",
zap.Int("port", 8080),
)
err := errors.New("connection refused")
logger.Error("dial failed",
zap.Error(err),
)
}zap.NewProduction() возвращает два значения: логгер и ошибку конфигурации — в учебном коде её отбрасывают через _, в боевом обрабатывают с fallback-логгером. defer logger.Sync() сбрасывает буферы перед выходом. Info и Error — методы записи: первым аргументом сообщение, дальше любое число полей. Запуск напечатает две строки: {"level":"info","ts":1712345678.12,"caller":"main/main.go:12","msg":"service started","port":8080} и {"level":"error","ts":...,"caller":"main/main.go:16","msg":"dial failed","error":"connection refused"} — у Error ещё появится стек-трейс, мы его в выводе сократили.
zap.NewProduction: читаем JSON по ключам#
Каждая запись production-пресета — однострочный JSON. level — серьёзность строчными: info, error. ts — время в секундах от эпохи, дробное число: так его дешевле сериализовать и хранить, а человеко-читаемый формат отдаётся интерфейсу, который показывает логи. caller — файл и строка, откуда произошёл вызов: без него «кто это залогировал» превращается в детектив. msg — ваше сообщение, короткая фраза-событие. Дальше идут ваши поля в том порядке, как вы их передали: port, order_id, error. На уровне Error и выше пресет добавит ключ stacktrace со стеком горутины — сразу видно, где именно случилось. Ещё в production-пресете спрятано сэмплирование: после ста одинаковых записей в секунду zap начинает прореживать поток, чтобы сервис при шторме ошибок не утонул в собственных логах — этой механике посвящён отдельный урок.
Ключи записи по умолчанию в production#
| Ключ | Что содержит | Кто его кладёт |
|---|---|---|
| level | серьёзность строчными: info, error | zap — по вызванному методу |
| ts | время: дробные секунды от эпохи | энкодер, автоматически |
| caller | файл:строка вызова | zap — вызовы включены по умолчанию |
| msg | сообщение-событие | первый аргумент Info и Error |
| ваши поля | port, order_id, error и прочие | вы — через zap.String и компанию |
| stacktrace | стек горутины | zap — для Error и выше |
zap.NewDevelopment: тот же код, другой вывод
GOpackage main
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewDevelopment()
defer logger.Sync()
logger.Debug("cache lookup",
zap.String("key", "user:42"),
zap.Bool("hit", false),
)
}Разница с прошлым примером — один конструктор: zap.NewDevelopment. Запуск покажет не JSON, а табличную строку: 2024-04-06T12:00:00.123+0300, табуляция, DEBUG, main/main.go:10, cache lookup и объект {"key": "user:42", "hit": false} в хвосте. Дата в человеко-читаемом формате, уровень заглавными, поля — колонкой в конце. Главное: Debug здесь виден — development-пресет стартует с уровня Debug, а не Info, поэтому отладочная запись доходит до терминала.
NewDevelopment: формат для человека#
NewDevelopment — пресет для терминала разработчика. Энкодер console пишет запись колонками: время в ISO-формате, уровень заглавными, место в коде, сообщение и объект с полями — глаз цепляет это мгновенно, без парсера в голове. Уровень по умолчанию — Debug: вы видите всё, включая отладочные записи, которые в production были бы отсечены. Стек-трейсы прилагаются раньше — с Warn, а не с Error: на машине разработчика лишний стек помогает поймать источник, а в проде он шумел бы в каждой второй записи. Режим development меняет и поведение DPanic — уровня, который паникует только в этом режиме; его разбор — в уроке про уровни логов. Итого: разработка — NewDevelopment, прод — NewProduction, и это не два конкурирующих вкуса, а два режима одного и того же логгера. Обратная конфигурация — production-пресет на ноутбуке — тоже встречается и заканчивается всегда одинаково: Debug-записи молчат, и половина отладки не работает; вместо колонок — однострочный JSON, который человек парсит глазами; стек-трейс на Warn не приходит, хотя именно его хотелось увидеть. Со стороны выглядит как «логгер сломался», на деле — просто не тот пресет.
Какой пресет выбрать: окружение решает#
Пресет — не вкусовщина, а свойство окружения. Локальная машина и тестовый стенд живут на NewDevelopment: человеко-читаемый вывод, Debug включён, стеки с Warn. Production — NewProduction: JSON для систем сбора, Info по умолчанию, сэмплирование. Правильный сервис выбирает пресет при старте, а не правкой кода между деплоями. Классика — переменная окружения: APP_ENV=production go run . — и логгер соберётся нужным. Если пресетов нужно больше — staging со своим уровнем, запись в файл вместо stdout, — это уже не выбор между двумя функциями, а zap.Config: полная конфигурация, которую можно описать в YAML и менять без пересборки; ей посвящён отдельный урок, а пока двух готовых пресетов хватает с головой. Законное исключение из правила — утилиты командной строки. Для CLI пользователь — человек в терминале, систем сбора нет, и JSON-простыня в stdout ломает пайплайн: demo | jq перестаёт работать, если между данными въелись логи. Для таких программ NewDevelopment остаётся даже в «проде»: релиз утилиты — тот же бинарник на машине пользователя, только без терминала разработчика. Что будет, если оставить в CLI production-пресет: каждая ошибка печатается JSON-строкой со стеком на десяток строк, пользователь видит {"level":"error"...} вместо человеческого объяснения, а в issues заводится вопрос «что это значит». Пресет — это интерфейс, и для терминала этим интерфейсом остаётся console-формат; к разделению потоков — данные в stdout, диагностика в stderr — вернёмся в уроке про приёмники.
Выбор пресета по APP_ENV
GOpackage main
import (
"os"
"go.uber.org/zap"
)
func main() {
var logger *zap.Logger
if os.Getenv("APP_ENV") == "production" {
logger, _ = zap.NewProduction()
} else {
logger, _ = zap.NewDevelopment()
}
defer logger.Sync()
logger.Info("up and running",
zap.String("env", os.Getenv("APP_ENV")),
)
}Обратите внимание на var logger zap.Logger до if: внутри веток используется присваивание =, а не :=, потому что переменная объявлена снаружи — иначе внутри if создалась бы своя, затенённая версия. Тип zap.Logger — именно его возвращают оба конструктора. Запустите с APP_ENV=production и без него и сравните две формы одной и той же записи.
logger.Sync() сбрасывает внутренние буферы в приёмник. Для stdout это чаще всего незаметно, но при записи в файл или в буферизованный вывод без Sync последняя пачка записей может не долететь до диска — программа завершится раньше, чем zap успеет дописать. Правило: defer logger.Sync() сразу после создания логгера, а не «допишу потом».
Создайте логгер один раз при старте и прокидывайте вниз по вызовам или кладите в структуру сервиса. Логгер на каждый вызов функции — лишние аллокации и расползающиеся настройки. Чтобы добавить контекст — модуль, request_id, — zap даёт методы With и Named: они не создают новый логгер, а возвращают дочерний с прицепленными полями.
Логгер — иммутабельная структура#
Тип *zap.Logger — иммутабельная структура: логгер хранит ядро (куда и как писать), уровень и набор опций вроде включённых вызовов. Иммутабельность даёт удобное свойство: логгер можно передавать по значению, держать в нескольких структурах и не бояться, что кто-то изменит его уровень под вами — With и Named не мутируют приёмник, а возвращают новый логгер с прицепленными полями или именем. Из этого следуют две практики. Первая: конструируйте логгер один раз в main и прокидывайте вниз — копирование дешёвое, пересоздание бессмысленное. Вторая: nil-логгер недопустим — если по пути зависимостей вместо настоящего логгера приехал nil, первая же запись упадёт с паникой; в тестах вместо nil подставляйте zap.NewNop() — заглушку, которая глотает записи. Кстати, именно иммутабельность делает безопасным выбор пресета по окружению из прошлого раздела: другой пресет — это другой логгер, а не перенастроенный старый.
Три ошибки первой недели#
Первая — потерянный Sync: программа пишет в файл, хвост записей пропадает при выходе, и виноватым назначают диск. Вторая — затенение логгера: logger, _ := zap.NewProduction() внутри ветки if создаёт локальную переменную, внешняя остаётся nil, и программа падает с паникой при первом же вызове — компилятор такое не ловит, а рантайм ловит редко и громко. Третья — логгер как глобальная переменная, присваиваемая из каждого пакета: настройки живут в трёх местах, тесты перезаписывают их друг у друга. Лечится просто: логгер рождается в main один раз и уезжает вниз через зависимости — структуру сервиса или параметр конструктора. Как безопасно пользоваться глобальным логгером, если без него никак, покажем в уроке про ReplaceGlobals — там же объясним, почему библиотечному коду он всё равно не нужен.
Сообщение и поля: что куда класть#
msg — короткая фраза-событие: что произошло, без данных внутри. «order created» — хорошо; «order A-1024 for user 42 created in 45ms» — плохо: данные в сообщении недоступны фильтрам, вы только что откатились в grep-мир из первого урока. Правило: факт — в msg, детали — в поля. «payment declined» плюс zap.String с причиной и zap.Int с суммой — запись и ищется, и считается, а не только читается. Из этого следует и дисциплина формулировок: одни и те же события во всём сервисе пишутся одинаково, иначе поиск по msg превращается в перечисление синонимов. Данные, которые нельзя утекать — пароли, номера карт, токены, — в лог не пишут вовсе: логи читает больше людей, чем кажется, и живут они дольше, чем хочется. zap честно напечатает всё, что вы ему отдали, — маскируйте чувствительное до вызова, а не после.
Проверка: пресеты
Что возвращает zap.NewProduction()?
Проверка: Sync
Зачем defer logger.Sync() в конце main?
Сколько строк напечатает программа?
Прочитайте код и выберите, что он выведет:
logger, _ := zap.NewProduction()
logger.Debug("cache hit", zap.String("key", "user:42"))
logger.Info("served", zap.Int("status", 200))Заполните пропуски в каркасе логгера
Соберите каркас: production-пресет, синхронизация и два метода записи — для обычного события и для провала.
Соберите программу с Error-записью
Расставьте строки в правильном порядке: импорты, логгер, Sync и запись ошибки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: пресеты и Sync
Что дальше по курсу#
Следующий урок — уровни логов: что значит каждый из семи, почему Fatal — это os.Exit(1) и когда Warn, а когда Error. Потом — поля: zap.String, zap.Duration и отчего zap.Any дорогой. Если многословность конструкторов давит, загляните в урок про SugaredLogger — упрощённый интерфейс с Infow и парами ключ-значение. Карта курса — на его странице, а пример zap logger-а из этого урока — лучший стартовый шаблон для ваших программ.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- go get go.uber.org/zap, затем logger, _ := zap.NewProduction() — логгер готов к работе.
- Production-запись: level, ts (секунды от эпохи), caller, msg, ваши поля и stacktrace с Error.
- NewDevelopment — консольный вывод колонками, уровень Debug, стеки с Warn.
- Выбор пресета — свойство окружения: APP_ENV при старте, а не правка кода между деплоями.
- defer logger.Sync() сразу после создания: буфер сбрасывается, хвост записей не теряется.
- Один логгер на приложение; контекст добавляют With и Named, а не новыми логгерами.
Домашнее задание
Соберите программу урока с APP_ENV: при значении production — NewProduction, иначе NewDevelopment. Запустите оба варианта и посчитайте разницу ключей в выводе. Потом перенаправьте вывод в файл командой >, уберите defer logger.Sync() и проверьте, теряется ли хвост записей при завершении — объясните результат в одном предложении.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем zap.NewProduction отличается от zap.NewDevelopment?
Production пишет JSON с уровнем Info и сэмплированием — для систем сбора логов. Development пишет консольные колонки с уровня Debug и стек-трейсами с Warn — для терминала разработчика.
Обязательно ли вызывать logger.Sync()?
Да, ставьте defer сразу после создания. Sync сбрасывает буферы: без него хвост записей может не долететь до файла, если программа завершится раньше.
Что делать со второй возвращаемой ошибкой конструктора?
NewProduction и NewDevelopment возвращают (*zap.Logger, error). В примерах ошибку отбрасывают, в боевом коде — обрабатывают: при провале берут запасной логгер и сообщают об этом другим способом.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Colly: парсинг и веб-скрапинг на Go
Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.
- Писать скраперы на Colly от первого Visit до обхода целого сайта
- Вынимать данные через CSS-селекторы и складывать в структуры Go
- Настраивать темп: задержки, параллелизм, лимиты на домен
- Работать с капризными сайтами: заголовки, cookies, прокси