Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 5 из 25 ~16 мин +50 XP

SugaredLogger: простой интерфейс

zap SugaredLogger — упрощённый интерфейс того же логгера: sugar := logger.Sugar(), и дальше поля передаются парами ключ-значение без единого конструктора. Разберём четыре семьи методов, ловушку нечётного числа аргументов, цену сахара в аллокациях и места, где сахар оправдан, а где мы закрываем его обратно через Desugar.

Содержание урока · 11 разделов

Семь конструкторов полей в одном вызове — ещё не рекорд: в 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: запустить программу с сахаром занимает минуту, а увидеть разницу своими глазами стоит этих пяти команд.

Первая запись на сахаре

GO
package 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)Printаргументы склеены через пробел, полей нет
wsugar.Infow(msg, "port", 8080)структурные парызапись с полем port=8080
fsugar.Infof("port %d", 8080)printfглаголы развёрнуты прямо в msg
lnsugar.Infoln("up", 8080)Printlnаргументы через пробел с переводом строки

Нечётное число аргументов

GO
package 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 не поймается ничем — в записи окажутся два разных поля с двумя разными именами, и поиск начнёт находить половину событий. Пары ключ-значение — цена сахара: скорость написания в обмен на потерю типовой проверки, а в горячем коде за это платят ещё и аллокациями, о чём ниже.

Цена сахара: 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. Промежуточных случаев не существует: либо запись редкая и код дорог, либо наоборот.

Скрипт: логируем чтение конфига

GO
package 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")

Получите сахар и запишите пару

Соберите скелет: обёртка поверх логгера, сброс буферов и запись с парами ключ-значение.

package main import ( "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction() sugar := logger.() defer sugar.() sugar.("order paid", "order_id", "A-1024", "amount", 1490) }

Соберите хвост main на сахаре

Расставьте строки: от готового логгера до записей двух семей.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: сахарный интерфейс

Термин

SugaredLogger

нажмите, чтобы перевернуть
Определение

Обёртка над *zap.Logger с упрощённым API: пары ключ-значение вместо типизированных полей. Создаётся logger.Sugar(), возвращается обратно sugar.Desugar().

1 / 6

Что дальше по курсу#

Сахар решает вопрос «как удобно писать», следующий — «в каком виде писать»: в уроке про 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 и убедитесь, что типизированные методы снова работают — сравните обе формы одной записи глазами.

Личные заметкиПусто
Хранятся только в вашем браузере (Zap · SugaredLogger: простой интерфейс)

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

SugaredLogger медленнее Logger — насколько?

На записи с полями — заметно: обёртка пакует аргументы в срез интерфейсов и выясняет типы в рантайме. По бенчмаркам репозитория отрыв виден и в наносекундах, и в аллокациях. Для скриптов и холодных путей это неважно, для горячего пути берут Logger.

Можно ли смешивать logger и sugar в одном приложении?

Да, это штатный сценарий: обёртка построена поверх того же ядра, записи идут в один приёмник в одном формате. zap советует конвертировать на границах производительного кода: logger.Sugar() и sugar.Desugar() стоят дёшево.

Почему компилятор не ловит нечётное число аргументов в Infow?

Метод объявлен как keysAndValues ...interface{}: типы и чётность проверяются только в рантайме. Лечится дисциплиной оформления — пары построчно, ключ и значение рядом — и ревью, где нечётный хвост ищут специально.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Colly25 уроков~9 чпрактика25 уроков уже доступны

Colly: парсинг и веб-скрапинг на Go

Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.

  • Писать скраперы на Colly от первого Visit до обхода целого сайта
  • Вынимать данные через CSS-селекторы и складывать в структуры Go
  • Настраивать темп: задержки, параллелизм, лимиты на домен
  • Работать с капризными сайтами: заголовки, cookies, прокси