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

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

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

Урок 4 из 25 ~18 мин +50 XP

Структурированные поля: zap.String и друзья

zap.String и zap.Int — конструкторы полей zap: маленькие структуры, которые превращают запись лога из фразы в документ. Поля zap — то, ради чего эта библиотека вообще появилась: тип известен в момент компиляции, рефлексия не нужна, сериализация летит. В этом уроке — вся семья конструкторов, цена универсального zap.Any и соглашение об именах.

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

Поле против форматной строки#

До структурных логов сообщение собирали строкой: fmt.Sprintf("order %s for user %d failed", orderID, userID). Форматная строка удобна ровно один раз — в момент написания. Дальше начинается цена: чтобы найти все заказы пользователя, нужен парсер, который знает, что после order идёт идентификатор, а user стоит до слова failed. Любая правка формулировки — «order %s of user %d was not processed» — ломает парсер и все дашборды на нём. Числа превращаются в текст и обратно: это лишняя работа и потеря типов — elapsed стал «45ms» и сортировать его по-настоящему нельзя. Поле zap устроено иначе: zap.String("order_id", orderID) — маленькая структура с ключом, номером типа и значением. Тип известен в момент компиляции, рефлексия не нужна, значение сериализуется напрямую в буфер. Запись собирается из полей как из деталей конструктора, и любое поле становится фильтром в системе сбора логов — без единой регулярки. Заметьте: Sprintf выполняется всегда, даже когда лог отсечён уровнем, — а типизированное поле на отключённом уровне не тратит ничего.

zap.String и друзья: конструкторы полей#

Конструкторы полей zap живут в самом пакете и названы по типу значения: zap.String для строк, zap.Int для int, zap.Bool, zap.Float64 и соседи по разрядности, zap.Duration для time.Duration, zap.Time для time.Time. Каждый возвращает zap.Field — структуру с именем ключа, номером типа и значением. Сериализация идёт напрямую: int упаковывается в JSON-число, Duration — в дробные секунды, Bool — в true или false. Отсюда скорость: на типизированном поле нет интерфейсов, рефлексии и лишних аллокаций — конструктор почти ничего не стоит, и именно это делает zap быстрым на горячем пути. Ниже — карта основных конструкторов; полная семья в документации пакета исчисляется десятками, но девяносто процентов жизни — восемь-десять штук из таблицы. Запомните принцип, а не список: имя конструктора = тип значения, и вы никогда не ошибётесь.

Основные конструкторы полей zap#

КонструкторЗначениеВ production-JSON
zap.String("order_id", id)string"order_id":"A-1024"
zap.Int("port", p)int"port":8080
zap.Bool("hit", b)bool"hit":false
zap.Float64("ratio", r)float64"ratio":0.87
zap.Duration("elapsed", d)time.Duration"elapsed":0.045 — секунды дробью
zap.Time("created_at", t)time.Time"created_at":1712345678.9 — эпоха
zap.Error(err)error"error":"connection refused"
zap.Strings("tags", s)[]string"tags":["new","paid"]

Типичная ошибка: числа в zap.String#

Самая живучая ошибка переходного периода — zap.String для чисел: zap.String("amount", "1490") вместо zap.Int. В JSON она почти незаметна: "amount":"1490" против "amount":1490 — разница в кавычках, и глаз её пропускает. Дальше начинается цена: в системе сбора поле становится строкой, сортируется как текст — 10 оказывается меньше 9, — фильтр amount > 1000 не работает, а график суммы заказов строится только через дорогой разбор на стороне запроса. То же с длительностью: "elapsed":"45ms" — красивая строка, из которой перцентили не посчитать; zap.Duration даёт число секунд, по которому p99 строится мгновенно. Что будет, если ошибку не поймать вовремя: тип поля система сбора фиксирует по первым записям, и поздний переезд со строки на число превращается в миграцию схемы с переиндексацией и перенастройкой дашбордов — не та правка, которую делают между делами. Ловится всё на ревью по одному взгляду на кавычки в примере записи, а локально — прогоните пример и посмотрите JSON глазами: тип каждого значения виден сразу. Правило то же, что и с Any: тип есть — берите типизированный конструктор; число в строке — это не формат, а потеря данных, которые вы ещё вчера обещали дашбордам.

Редкие конструкторы, которые выручат#

Кроме базовой восьмёрки в пакете zap живут конструкторы для особых случаев. zap.Binary(key, []byte) упакует байты в base64-строку — удобно для хэшей и идентификаторов протоколов. zap.Reflect(key, value) сериализует что угодно через рефлексию — это и есть механизм zap.Any для незнакомых типов. zap.Named(key, field) переименовывает готовое поле: взяли из библиотечной обёртки zap.Int с ключом n, назвали по-своему. zap.Group("user", zap.String("id", "42"), zap.String("role", "admin")) — современная альтернатива Namespace: поля группируются в объект на месте, без «открытия» и «закрытия». Пользуйтесь ими точечно: экзотика в логах — как соль в выпечке, её должно быть видно чуть-чуть. В девяноста процентах записей вам понадобится String, Int, Duration и Error — остальное вы вспомните, когда оно реально понадобится.

Запись со всеми базовыми типами

GO
package main

import (
        "time"

        "go.uber.org/zap"
)

func main() {
        logger, _ := zap.NewProduction()
        defer logger.Sync()

        start := time.Now()

        logger.Info("order processed",
                zap.String("order_id", "A-1024"),
                zap.Int("amount", 1490),
                zap.Bool("paid", true),
                zap.Float64("weight", 2.5),
                zap.Duration("elapsed", time.Since(start)),
                zap.Time("created_at", time.Now()),
        )
}

Шесть полей — шесть типов, и все сериализуются без рефлексии. В production-JSON запись выглядит так: {"level":"info","ts":...,"caller":"main/main.go:16","msg":"order processed","order_id":"A-1024","amount":1490,"paid":true,"weight":2.5,"elapsed":0.046,"created_at":1712345678.9}. Заметьте: Duration и Time превратились в числа — формат значения решает энкодер, и его можно поменять, о чём разговор в уроке про энкодеры. Запускайте локально: песочница zap не исполняет.

zap.Any: универсальный, но дорогой#

Когда значение приходит с типом any — из карты, из конфига, из чужой библиотеки — есть zap.Any: конструктор, который принимает что угодно и на лету разбирается, что это. Цена: внутри — интерфейс, а для незнакомых типов — рефлексия через zap.Reflect. Аллокации на горячем пути, против которых мы боролись весь курс, возвращаются: по замерам из репозитория типизированные поля на порядки дешевле Any с произвольной структурой. Моё правило: Any — для холодного кода, который выполняется раз при старте, например залогировать конфиг, и для значений, у которых тип всё равно не вытащить. На каждый запрос — типизированные конструкторы. Если поле встречается часто, не ленитесь: один раз вытащите тип — и получите и скорость, и честный JSON вместо сериализации через рефлексию, которая к тому же может напечатать не то, что вы ожидали. А если значение приходит раз в жизни процесса — смело берите Any и не делайте себе работу.

Ошибки: zap.Error и zap.Errors#

Ошибка — настолько частое поле, что у неё свой конструктор: zap.Error(err) кладёт значение под ключ error строкой — так, как её возвращает err.Error(). Внутри нет магии: конструктор понимает интерфейс error и сериализует текст. Если оборачиваете ошибку через fmt.Errorf с %w, в лог попадёт текст всей цепочки — как обычно и нужно при разборе. Для пачки ошибок есть zap.Errors: срез ошибок превратится в JSON-массив строк — удобно для батчей, где упало несколько элементов сразу. Отдельная история — стек-трейсы: на Error и выше production-пресет добавит их сам, а заставить стек на любом уровне можно опцией AddStacktrace; тонкости выбора, где стек уместен, а где он шум, разберём в уроке про ошибки и стеки. Практика же проста: любая запись уровня Warn и Error несёт zap.Error — без него расследование начинается с гадания, что именно не так.

Ошибка, пачка ошибок и вложенность

GO
package main

import (
        "errors"

        "go.uber.org/zap"
)

func main() {
        logger, _ := zap.NewProduction()
        defer logger.Sync()

        err := errors.New("connection refused")
        logger.Warn("db unavailable", zap.Error(err))

        batch := []error{
                errors.New("timeout"),
                errors.New("quota exceeded"),
        }
        logger.Error("batch failed",
                zap.Int("total", 120),
                zap.Errors("failures", batch),
                zap.Namespace("http"),
                zap.Int("status", 500),
        )
}

zap.Error напечатает "error":"connection refused", а zap.Errors соберёт массив: "failures":["timeout","quota exceeded"]. zap.Namespace устроен иначе: он не несёт значения, а открывает вложенный объект — все поля после него лягут внутрь: "http":{"status":500}. Так группируют связанные поля, чтобы не плодить имена вида http_status и http_method.

Вложенность и массивы#

Namespace — самый простой способ группировки: после zap.Namespace("http") каждый следующий конструктор пишет в открытый объект, пока другой Namespace его не сменить. Массивы — своя семья конструкторов: zap.Strings для среза строк, zap.Ints для чисел, zap.Errors для ошибок — JSON-массив вместо цикла с конкатенацией. Вложенность — вещь полезная, но с дозировкой: каждый уровень объекта — это точка в пути фильтра, и http.status в системе сбора — уже другой ключ, чем просто status; глубоко вложенный JSON люди читать перестают. Я держу правило: один уровень вложенности на источник — http, db, user; глубже — только если без этого поле путается с тёзкой из другого источника. Плоская запись с продуманными именами почти всегда удобнее трёхэтажных объектов, а миграции таких записей между системами сбора проходят без сюрпризов. Массивы без нужды тоже не плодите: список из двух элементов читается лучше, чем объект-оглавление с полем count.

Имена полей: договоритесь заранее#

Ключ поля — контракт на годы: по нему строятся дашборды, алерты и запросы в системе сбора, и переименовать его дороже, чем выбрать правильно. Практика индустрии: snake_case и нижний регистр — user_id, а не userId или UserID; короткие, но не огрызки: elapsed_ms понятнее, чем e или dur; единицы измерения — в имени: elapsed_ms, size_bytes, иначе поле живёт своей неопределённой жизнью и портит графики. Стабильность важнее красоты: если поле называется order_id, не заводите рядом orderID и orderId в соседнем сервисе — для систем сбора это три разных ключа, и каждый потребует своего правила разбора. Словарь полей стоит держать в одном месте — хотя бы в markdown рядом с кодом: какие поля обязательны для запросов, как называются идентификаторы, где единицы. Пятнадцать минут договорённости на старте проекта экономят недели разборов чужих графиков. Переименование — отдельная боль: если ключ всё-таки пришлось поменять, системы сбора требуют перенастройки правил, дашбордов и алертов — закладывайте переименование в план релиза, а не делайте между делами.

Проверка: выбор конструктора

Каким конструктором записать значение типа time.Duration?

Проверка: цена zap.Any

Чем плох zap.Any на горячем пути?

Как лягут поля с Namespace?

Прочитайте код и выберите, что он выведет:

logger.Info("request done",
        zap.Namespace("http"),
        zap.Int("status", 500),
        zap.String("method", "POST"),
)

Что окажется под ключом failures?

Прочитайте код и выберите, что он выведет:

errs := []error{errors.New("timeout"), errors.New("no space")}
logger.Error("batch failed", zap.Errors("failures", errs))

Заполните пропуски в записи с полями

Соберите запись: сброс буфера, длительность, флаг и ошибка — каждая своим конструктором.

package main import ( "errors" "time" "go.uber.org/zap" ) func main() { logger, _ := zap.NewProduction() defer logger.() err := errors.New("timeout") logger.Warn("slow call", zap.("elapsed", 90*time.Millisecond), zap.("retried", true), zap.(err), ) }

Соберите запись с полями

Расставьте строки программы в правильном порядке — от пакета до записи с длительностью.

Строки

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

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

Карточки: конструкторы полей

Термин

zap.String

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

Конструктор строкового поля: zap.String("order_id", id). Тип известен при компиляции — сериализация без рефлексии.

1 / 6

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

Поля и уровни — вся дневная работа с логгером; дальше начинается инженерия. SugaredLogger упрощает запись: Infow с парами ключ-значение без конструкторов — удобство за небольшую плату в скорости. Энкодеры решают, как поля станут строкой: почему elapsed — число, а время — эпоха, и как получить человеко-читаемый формат. Приёмники вывода отправляют записи в stdout, файл или оба сразу. А чтобы увидеть всё в связке с базой: медленный SQL-запрос с полями query, latency и rows — так мы будем логировать GORM в шестнадцатом уроке; самому ORM посвящён отдельный курс.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Поле zap — типизированная структура Field: ключ, номер типа, значение; сериализация без рефлексии.
  • Базовые конструкторы: String, Int, Bool, Float64, Duration, Time, Error.
  • zap.Any принимает что угодно, но платит интерфейсом и рефлексией — только для холодных путей.
  • zap.Errors — срез ошибок в JSON-массив; zap.Namespace открывает вложенный объект.
  • Имена полей: snake_case, единицы в имени (elapsed_ms), стабильность во всех сервисах.
  • Формат значения решает энкодер: Duration и Time в production-JSON превращаются в числа.

Домашнее задание

Возьмите функцию, которая делает работу, или имитируйте её time.Sleep. Залогируйте завершение вызовом Info с полями четырёх типов: строка, число, Bool и Duration через time.Since(start). Затем сгруппируйте два поля в Namespace и проверьте JSON глазами. В конце придумайте три имени полей в snake_case с единицами измерения для своего проекта.

Личные заметкиПусто
Хранятся только в вашем браузере (Zap · Структурированные поля: zap.String и друзья)

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

Почему zap.String быстрее fmt.Sprintf?

Конструктор поля хранит тип в структуре Field, и сериализация идёт напрямую в буфер без рефлексии. Sprintf каждый раз парсит форматную строку и строит промежуточную строку с аллокациями.

Когда уместен zap.Any?

Для холодных путей: логирование конфига при старте, значения с неизвестным заранее типом. На горячем пути Any означает интерфейс и рефлексию — лишние аллокации на каждой записи.

В каком стиле называть поля zap?

snake_case и нижний регистр: user_id, elapsed_ms. Единицы — в имени, имена — стабильные во всех сервисах: для систем сбора user_id и userId — два разных ключа.

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

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

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

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

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

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

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

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

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