Содержание урока · 13 разделов
- Поле против форматной строки
- zap.String и друзья: конструкторы полей
- Основные конструкторы полей zap
- Типичная ошибка: числа в zap.String
- Редкие конструкторы, которые выручат
- zap.Any: универсальный, но дорогой
- Ошибки: zap.Error и zap.Errors
- Вложенность и массивы
- Имена полей: договоритесь заранее
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Поле против форматной строки#
До структурных логов сообщение собирали строкой: 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 — остальное вы вспомните, когда оно реально понадобится.
Запись со всеми базовыми типами
GOpackage 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 — без него расследование начинается с гадания, что именно не так.
Ошибка, пачка ошибок и вложенность
GOpackage 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))Заполните пропуски в записи с полями
Соберите запись: сброс буфера, длительность, флаг и ошибка — каждая своим конструктором.
Соберите запись с полями
Расставьте строки программы в правильном порядке — от пакета до записи с длительностью.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: конструкторы полей
Что дальше по курсу#
Поля и уровни — вся дневная работа с логгером; дальше начинается инженерия. 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.String быстрее fmt.Sprintf?
Конструктор поля хранит тип в структуре Field, и сериализация идёт напрямую в буфер без рефлексии. Sprintf каждый раз парсит форматную строку и строит промежуточную строку с аллокациями.
Когда уместен zap.Any?
Для холодных путей: логирование конфига при старте, значения с неизвестным заранее типом. На горячем пути Any означает интерфейс и рефлексию — лишние аллокации на каждой записи.
В каком стиле называть поля zap?
snake_case и нижний регистр: user_id, elapsed_ms. Единицы — в имени, имена — стабильные во всех сервисах: для систем сбора user_id и userId — два разных ключа.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Colly: парсинг и веб-скрапинг на Go
Интерактивный курс Colly — фреймворка веб-скрапинга для Go: селекторы, пагинация, формы, прокси, лимиты, кэш, хранение данных. 25 уроков и итоговый проект.
- Писать скраперы на Colly от первого Visit до обхода целого сайта
- Вынимать данные через CSS-селекторы и складывать в структуры Go
- Настраивать темп: задержки, параллелизм, лимиты на домен
- Работать с капризными сайтами: заголовки, cookies, прокси