Содержание урока · 11 разделов
- logger.Sugar(): как получить SugaredLogger
- Infow, Warnf, Infoln: три семьи методов
- Четыре семьи методов на примере Info
- Ловушка нечётного числа аргументов
- Цена сахара: any и аллокации
- Когда sugar logger оправдан
- Desugar(): вернуть типизированный Logger
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Семь конструкторов полей в одном вызове — ещё не рекорд: в HTTP-обработчике их бывает дюжина, и вызов Info обрастает хвостом из zap.String, zap.Int и zap.Duration, который переносится на три строки редактора. Код читается, но печатать утомительно, а прототип требует скорости: повесить лог с пятью значениями за десять секунд. Для таких мест у zap есть второй интерфейс — SugaredLogger, «сахарный» логгер: те же уровни, тот же вывод, но поля передаются парами ключ-значение без конструкторов. Это не отдельная библиотека и не другой формат записи — обёртка над тем же *zap.Logger, поверх того же ядра и того же приёмника. В этом уроке разберём, как её получить, что она умеет, где подставляет подножку и в каких местах я её включаю в своих сервисах, а где закрываю обратно.
logger.Sugar(): как получить SugaredLogger#
Вызов logger.Sugar() возвращает *zap.SugaredLogger — тонкую обёртку, которая хранит исходный логгер внутри. Уровни, энкодер, приёмник, поля, добавленные через With, — всё общее: запись через sugar уезжает туда же и в том же виде, что и запись через logger. Стало быть, можно держать в приложении оба интерфейса: типизированный Logger для горячих путей и SugaredLogger для всего остального — zap сам советует конвертировать на границах производительного кода, а метод Desugar делает обратное превращение. Одно правило переносится с базового логгера без изменений: буферы. SugaredLogger буферизует так же, поэтому defer sugar.Sync() ставится сразу после создания — забытый Sync съедает хвост записей так же безжалостно, как и у Logger. Песочница сайта zap не исполняет, поэтому вся проверка примеров — в локальном терминале после go get go.uber.org/zap: запустить программу с сахаром занимает минуту, а увидеть разницу своими глазами стоит этих пяти команд.
Первая запись на сахаре
GOpackage main
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
sugar := logger.Sugar()
sugar.Infow("order paid",
"order_id", "A-1024",
"amount", 1490,
)
sugar.Warnf("slow call: %d ms", 210)
}logger.Sugar() создаёт обёртку — дальше вызовы идут от sugar. Infow принимает сообщение и любое число пар: ключи-строки чередуются со значениями любого типа, и на выходе получится знакомый JSON: "order_id":"A-1024", "amount":1490. Warnf — из printf-семейства: форматная строка и аргументы, как у fmt.Printf. Заметьте: defer висит на исходном logger — у обёртки те же буферы, а привычный defer sugar.Sync() тоже допустим; выберите один вариант и держитесь его во всём приложении.
Infow, Warnf, Infoln: три семьи методов#
У SugaredLogger на каждый уровень приходится по четыре метода, и запутаться в суффиксах проще, чем кажется. Семья без суффикса — Print-стиль: sugar.Info("up") печатает аргументы через пробел, как fmt.Print, без полей. Семья w — «with»: sugar.Infow(msg, ключ, значение, ключ, значение...) — структурная запись парами, читается «info with»; это главный метод урока. Семья f — printf-стиль: sugar.Infof("paid %d", amount) — форматная строка с глаголами, удобно для готовых фраз. Семья ln — Println-стиль: те же аргументы через пробел, но с переводом строки в конце. Нюанс, который экономит время на поисках в документации: Infowf не существует — у пар ключ-значение нет printf-версии, потому что смешивать форматную строку и произвольные пары в одном вызове было бы перебором. Зато в w-семью можно подмешивать готовые поля: zap.Error(err) внутри Infow — легальный приём. И голая ошибка сахару по силам: передали err без ключа — zap сам обернёт её в поле error; вторая ошибка без ключа в том же вызове уже не уместится и уедет в отдельную служебную запись.
Четыре семьи методов на примере Info#
| Семья | Вызов | Стиль | Что получится |
|---|---|---|---|
| без суффикса | sugar.Info("up", 8080) | аргументы склеены через пробел, полей нет | |
| w | sugar.Infow(msg, "port", 8080) | структурные пары | запись с полем port=8080 |
| f | sugar.Infof("port %d", 8080) | printf | глаголы развёрнуты прямо в msg |
| ln | sugar.Infoln("up", 8080) | Println | аргументы через пробел с переводом строки |
Нечётное число аргументов
GOpackage main
import (
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
defer logger.Sync()
sugar := logger.Sugar()
sugar.Warnw("payment retry",
"attempt", 2,
"order_id",
)
}Ключ order_id остался без значения — поприветствуйте самую частую ошибку сахарного кода, которую компилятор не заметит: все аргументы типа any. zap не паникует и не молчит: пара attempt=2 уйдёт в запись как положено, а хвостовой ключ породит отдельную запись уровня error с сообщением «Ignored key without a value.» и полем ignored. Основная запись уедет без order_id — фильтр по нему в системе сбора вернёт пустоту. Запустите локально и посмотрите на обе строки: эффект запоминается быстрее любого текста.
Ловушка нечётного числа аргументов#
Компилятор пропускает нечётные пары молча, типы проверять некому, и на ревью такой хвост виден только внимательным глазом. Последствия двухслойные. Во-первых, в основной записи не хватает поля — все дашборды и алерты, завязанные на order_id, его не увидят, и статистика тихо врёт в меньшую сторону. Во-вторых, появляется служебная error-запись, которой не было в замысле: в проде с правилом «все Error — дежурному» она окажется ложным срабатыванием. Отсюда дисциплина: пары печатаются в вызове построчно — ключ и значение рядом, и выравнивание отступами, как в примере выше, ловит половину таких ошибок; нечётное число строк в вызове — сигнал пересчитать аргументы. Вторая ловушка рядом: ключ — это просто строка, и опечатка order_id вместо order_id не поймается ничем — в записи окажутся два разных поля с двумя разными именами, и поиск начнёт находить половину событий. Пары ключ-значение — цена сахара: скорость написания в обмен на потерю типовой проверки, а в горячем коде за это платят ещё и аллокациями, о чём ниже.
Нечётный хвост в Infow печатает в лог запись уровня error. На сервисе с алертом «Error — повод звонить» пара таких опечаток за неделю приучит дежурного игнорировать алерты, а это дороже пропавшего поля. Ключ без значения — баг: правьте на месте, а не фильтруйте потом.
Цена сахара: any и аллокации#
Каждый вызов Infow упаковывает аргументы в срез []interface{} — это аллокация на запись, а значения едут с типом any, и zap выясняет, что с ними делать, уже в рантайме. Типизированные поля из прошлых уроков устроены иначе: zap.String("order_id", id) — готовая структура Field, у которой тип известен при компиляции, а сериализация идёт напрямую в буфер. По бенчмаркам репозитория SugaredLogger на записи с полями стабильно проигрывает базовому Logger — и по наносекундам на вызов, и особенно по числу аллокаций; точные цифры зависят от машины, но порядок виден на любом прогоне из README. На скрипте, который запускается раз в сутки, это неважно. На обработчике с тысячей запросов в секунду каждая аллокация на логе складывается в заметную долю нагрузки на сборщик мусора — тот самый мусор, против которого zap и строился. Отсюда правило простое, как молоток: сахар — по краям приложения, типизированный Logger — на горячем пути, и граница между ними проходит через Desugar.
Когда sugar logger оправдан#
Сахар стоит своих денег в трёх местах. Первое — скрипты и CLI-утилиты: они живут секунды, пишут десятки записей, и скорость написания важнее наносекунд исполнения; если строите командные утилиты системно, посмотрите на курс про Cobra — там про структуру таких программ подробный курс. Второе — обёртки и клей: функция, читающая конфиг, обработчик старта, импортёр данных — холодные пути, где Infow с парами читается быстрее, чем лестница конструкторов. Третье — прототипы: пока не ясно, какие поля понадобятся дашбордам, сахар позволяет двигаться, а переезд на типизированные поля потом — механическая работа на час. Где сахар мне не нравится: библиотечный код — заставлять пользователей тащить ваш вкус в зависимости странно, там лучше принимать *zap.Logger параметром; и горячие пути HTTP-обработчиков, где каждая запись считается на графике p99 — логирование таких запросов мы собираем в уроке про HTTP-middleware. Промежуточных случаев не существует: либо запись редкая и код дорог, либо наоборот.
Скрипт: логируем чтение конфига
GOpackage main
import (
"os"
"go.uber.org/zap"
)
func main() {
logger, _ := zap.NewProduction()
sugar := logger.Sugar()
defer sugar.Sync()
data, err := os.ReadFile("config.yaml")
if err != nil {
sugar.Errorw("config read failed",
"path", "config.yaml",
zap.Error(err),
)
os.Exit(1)
}
sugar.Infow("config loaded",
"path", "config.yaml",
"bytes", len(data),
)
}Классический скриптовый сценарий: короткая программа с несколькими точками выхода. Errorw с парами описывает провал, Infow — успех; в Errorw подмешан zap.Error(err) — готовые поля и пары живут рядом без конвертаций. defer sugar.Sync() один на всю программу: сахар не отменяет требование сбросить буферы перед выходом. Для программы, которая выполняется один раз, цена any-аргументов незаметна — зато код читается с листа.
Desugar(): вернуть типизированный Logger#
Обратное превращение — sugar.Desugar() — возвращает *zap.Logger, спрятанный внутри обёртки. Стоимость мала: по документации десугарить разумно хоть на каждой границе модуля. Отсюда гибридная схема, которую я считаю рабочим дефолтом среднего сервиса: приложение в целом живёт на сахаре — писать и читать удобно, — а внутри модулей с высокой нагрузкой первый же вызов Desugar возвращает типизированный Logger, и дальше работают zap.String с компанией. У обёртки, впрочем, есть собственные With и Named: sugar.With("service", "billing") вернёт SugaredLogger с прицепленными полями, Named добавит подскоп в имя — часть контекстной работы доступна и без десугаринга. Про прокидывание контекста по дереву вызовов — отдельный урок про With и Named, здесь достаточно факта: sugar и logger конвертируются в обе стороны дёшево, а значит, выбор интерфейса — не приговор на весь проект, а решение для конкретного модуля.
Проверка: нечётные аргументы
Что сделает zap с вызовом sugar.Infow("paid", "order_id", "A-1024", "amount")?
Проверка: выбор интерфейса
Где SugaredLogger уместен с чистой совестью?
Что напечатает printf-вызов?
Прочитайте код и выберите, что он выведет:
sugar := logger.Sugar()
sugar.Infof("paid %s for %d", "A-1024", 1490)Дойдет ли Debug через сахар?
Прочитайте код и выберите, что он выведет:
logger, _ := zap.NewProduction()
defer logger.Sync()
sugar := logger.Sugar()
sugar.Debugw("cache miss", "key", "user:42")Получите сахар и запишите пару
Соберите скелет: обёртка поверх логгера, сброс буферов и запись с парами ключ-значение.
Соберите хвост main на сахаре
Расставьте строки: от готового логгера до записей двух семей.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: сахарный интерфейс
Что дальше по курсу#
Сахар решает вопрос «как удобно писать», следующий — «в каком виде писать»: в уроке про JSON и console энкодеры разбираем два формата вывода и то, чем прод отличается от ноутбука разработчика. Если вы дошли до гибридной схемы с Desugar, карта остальных тем — на странице курса: дальше начинается настройка — конфиг, приёмники, ротация.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- logger.Sugar() возвращает SugaredLogger — обёртку над тем же ядром: уровни, энкодер и приёмник общие.
- Четыре семьи методов: без суффикса (Print), w (пары ключ-значение), f (printf), ln (Println); Infowf не существует.
- Нечётное число аргументов порождает error-запись «Ignored key without a value.» — поле в основной записи не появляется.
- Цена сахара: упаковка аргументов в []interface{} — аллокации на каждой записи; горячий путь остаётся за типизированным Logger.
- Сахар оправдан в скриптах, обёртках и прототипах; sugar.Desugar() дёшево возвращает типизированный Logger.
- defer sugar.Sync() обязателен: буферы у обёртки те же, что у исходного логгера.
Домашнее задание
Возьмите программу из урока про поля и перепишите её на SugaredLogger: logger.Sugar(), три вызова — Infow с двумя парами, Infof с форматной строкой и Warnw с подмешанным zap.Error. Затем добавьте четвёртый вызов с нечётным числом аргументов и посмотрите на служебную error-запись в терминале. В конце закройте сахар вызовом Desugar и убедитесь, что типизированные методы снова работают — сравните обе формы одной записи глазами.
Личные заметкиПусто
Вопросы и ответы по уроку
SugaredLogger медленнее Logger — насколько?
На записи с полями — заметно: обёртка пакует аргументы в срез интерфейсов и выясняет типы в рантайме. По бенчмаркам репозитория отрыв виден и в наносекундах, и в аллокациях. Для скриптов и холодных путей это неважно, для горячего пути берут Logger.
Можно ли смешивать logger и sugar в одном приложении?
Да, это штатный сценарий: обёртка построена поверх того же ядра, записи идут в один приёмник в одном формате. zap советует конвертировать на границах производительного кода: logger.Sugar() и sugar.Desugar() стоят дёшево.
Почему компилятор не ловит нечётное число аргументов в Infow?
Метод объявлен как keysAndValues ...interface{}: типы и чётность проверяются только в рантайме. Лечится дисциплиной оформления — пары построчно, ключ и значение рядом — и ревью, где нечётный хвост ищут специально.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
- JSON или console: выбор энкодера
- zap.Config: полная настройка логгера
- Куда пишем: stdout, файлы и мультиплексирование
- Ротация логов: lumberjack
- Структурированные поля: zap.String и друзья
- Уровни логов: Debug, Info, Warn, Error
- Первый логгер: NewProduction и NewDevelopment
- Знакомство с zap: зачем структурные логи
- Все курсы по Go
Следующая ступень учебного пути
Colly: парсинг и веб-скрапинг на Go
Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.
- Писать скраперы на Colly от первого Visit до обхода целого сайта
- Вынимать данные через CSS-селекторы и складывать в структуры Go
- Настраивать темп: задержки, параллелизм, лимиты на домен
- Работать с капризными сайтами: заголовки, cookies, прокси