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

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

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

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

Глобальный логгер: ReplaceGlobals

Вызов zap ReplaceGlobals (в коде — zap.ReplaceGlobals) превращает no-op zap.L() в боевой логгер одной строкой — самый быстрый способ достучаться до пакетов, куда зависимость-логгер не дотянута. Ниже — когда глобальный логгер go-проекта оправдан, чем грозит гонка на старте и подмена в тестах, и почему рабочий паттерн звучит скучно: настраиваем в main, отдаём явно.

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

История с одним и тем же началом: в большой проект подключена библиотека аналитики, из её глубины должны идти записи, но в системе сбора от неё пусто. Код библиотеки безупречен — он пишет через zap.L(), глобальный логгер пакета zap. Ни один тест не падает: вызовы проходят, записи просто никуда не попадают. Причина в том, что глобал по умолчанию — заглушка, а zap.ReplaceGlobals в main никто не вызвал, потому что о договорённости никто не знал. Глобальный логгер go-программы — скрытая зависимость уровня процесса: он либо настроен до первого вызова, либо молча съедает телеметрию. Разберём, как работает пара zap.L() и ReplaceGlobals, где глобал действительно нужен и почему зрелые проекты настраивают его один раз в main, а дальше передают логгер явно через конструкторы.

zap.L() и zap.S(): что лежит в глобале#

В пакете zap живут две глобальные переменные: L для zap.Logger и S для zap.SugaredLogger. До первого вызова ReplaceGlobals обе указывают на заглушки: записи уходят в никуда, Sync возвращает nil, паники нет — код работает и молчит. zap.ReplaceGlobals(logger) меняет обе переменные разом и возвращает функцию restore, которая восстанавливает прежние значения. Подмена безопасна для читателей: внутри глобала мьютекс, data race не возникнет — опасность в другом, в порядке вызовов, и об этом ниже. Читать глобал дёшево: zap.L() снимает блокировку на чтение и отдаёт текущее значение. Полезно понимать механику: zap.L() каждый раз возвращает актуального логгера, а вот переменная, в которую вы скопировали zap.L() однажды, остаётся старым логгером навсегда — на этом копировании спотыкаются чаще, чем на самой подмене.

Минимальная настройка в main

GO
func main() {
    logger, err := zap.NewProduction()
    if err != nil {
        panic(err)
    }
    defer logger.Sync()

    restore := zap.ReplaceGlobals(logger)
    defer restore()

    srv := server.New(":8080", zap.L().Named("server"))
    if err := srv.ListenAndServe(); err != nil {
        zap.L().Fatal("server stopped", zap.Error(err))
    }
}

ReplaceGlobals вызывается сразу после сборки: с этого момента zap.L() и zap.S() во всех пакетах пишут в production-конфиг. restore вешается на defer — не столько для этого процесса, сколько для тестов и CLI-подкоманд, где глобал меняют и возвращают. Сервер при этом получает своего Named-потомка явно: глобал здесь страховка для аварийных путей, а не основной канал зависимостей. zap.L().Named("server") читает глобал в момент вызова: сервер фиксирует потомка раз и навсегда, последующие подмены на него не влияют — копия zap.L() в поле структуры замораживает значение, о чём говорит predict ниже.

Зачем глобальный логгер go-коду: три честных случая#

Случай первый — утилита в main-пакете: пять файлов, два флага, DI-цепочка длиной в одну функцию; глобал здесь дешевле контекста, и это нормально. Случай второй — граница с чужим кодом: библиотека пишет через zap.L(), и её сигнатуры вы поменять не можете; тогда пара zap ReplaceGlobals и zap.L() — единственный шов, куда записи библиотеки вообще попадут. Случай третий — аварийные пути, до которых контекст не доезжает: хендлер паники в горутине, обработчик сигналов, финализатор процесса. Во всех трёх глобал — запасной выход, а не архитектура. Признак здорового отношения к глобалу: основной поток приложения передаёт логгер явно — конструкторам, структурам, контексту, — а zap.L() остаётся страховкой, на которую смотрят при разборе падений.

Проверка: что в глобале до настройки

zap.ReplaceGlobals ещё не вызывался. Что вернёт zap.L() и что будет с записью Info?

Опасность первая: порядок инициализации#

Опасность глобала — не data race: подмена защищена мьютексом, и race detector ругаться не будет. Опасность — в порядке вызовов. init-функции пакетов выполняются до main, и если библиотека пишет в zap.L() из init, на тот момент глобал ещё заглушка. Если ReplaceGlobals зовут в init другого пакета, порядок определяет граф импортов — нечитаемая зависимость, которая меняется от одной перестановки импорта в файле. Горутины, стартовавшие до подмены — фоновая очистка, прогрев кеша, запущенный из инициализатора, — тоже пишут в заглушку, и их записи теряются без следа. Диагноз ставится по одному симптому: записи есть у одних пакетов и нет у других, а перезапуск не меняет ничего. Лечение всегда одно: глобал настраивается в main до запуска горутин, а не в глубине импортов.

Ловушка init-функции

GO
// package analytics — библиотечный код
package analytics

import "go.uber.org/zap"

func init() {
    // Выполнится ДО main: глобал ещё заглушка.
    zap.L().Info("analytics initialized") // запись потеряна
}

Инициализаторы исполняются до main в порядке зависимостей импортов, поэтому ReplaceGlobals из main ещё не был вызван. Запись уходит в no-op без ошибок и предупреждений. Правило для библиотек: не писать в глобал из init вовсе — телеметрия старта библиотека отдаёт при явном вызове конструктора, куда хост передаёт настроенный логгер.

Опасность вторая: тесты#

Глобал — состояние всего процесса, и тесты делят его между собой. Заменили глобал в одном тесте и не вернули — следующий тест пишет в чужого наблюдателя или молчит. С t.Parallel() картина грустнее: параллельные тесты дерутся за один глобал, и результат зависит от планировщика, то есть флакает. Дисциплина простая: zap.ReplaceGlobals возвращает restore, и defer restore() — обязательная пара к каждому вызову в тесте; а параллельный тест, которому нужен глобал, — повод переписать его на явную передачу логгера, как делает observer из урока про тестирование логов. Прогон таких тестов — обычный go test на локальном Go, никакой обвязки не нужно.

Тест с restore

GO
func TestGlobalConfigured(t *testing.T) {
    core, logs := observer.New(zapcore.DebugLevel)
    restore := zap.ReplaceGlobals(zap.New(core))
    defer restore()

    doWork() // внутри — zap.L().Warn("degraded")

    if logs.FilterMessageSnippet("degraded").Len() == 0 {
        t.Fatal("expected a warning from doWork")
    }
}

Пара ReplaceGlobals и defer restore() в тесте нерушима: как только тест закончится, глобал вернётся к прежнему значению, и соседние тесты ничего не заметят. Проверки те же, что и для обычного логгера: уровень, сообщение, поля. Единственное отличие — код под тест берёт логгера из zap.L(), поэтому наблюдатель попадает внутрь через глобал.

Что будет, если ReplaceGlobals вызвать дважды#

Второй вызов не запрещён и не паникует: он подменяет текущее значение и возвращает restore, возвращающий уже его. Пары вызовов складываются в стек, откаты применяются в обратном порядке — это удобно в CLI с подкомандами: корень настроил глобал, подкоманда подменила логгера своим Named-потомком и вернула всё defer'ом. Две подмены без отката — уже запах дизайна: второй restore вернёт первого логгера, а не заглушку, и цепочка подмен растёт с каждым пакетом, решившим «подстроить глобал под себя». Правило из чек-листа ниже — один вызов на процесс — существует именно потому, что механика позволяет больше.

Что выведет каждая из двух записей?

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

early := zap.L()

logger, _ := zap.NewProduction()
_ = zap.ReplaceGlobals(logger)

early.Info("one")
zap.L().Info("two")

Паттерн: настраиваем в main, отдаём явно#

Здоровый проект выглядит так: main собирает конфиг и логгер — пресет или Build из урока про конфиг, — затем делает ReplaceGlobals один раз и дальше передаёт логгера по рукам: конструкторам сервера, репозиториев, посредников. Дети строятся через With и Named; иерархию имён разбирает урок про With и Named. Глобал при такой схеме остаётся для трёх случаев из начала урока. В тестах же логгер вообще не глобален: код под тест принимает *zap.Logger, и наблюдатель входит через конструктор. Спорное мнение в защиту глобала: в маленьких CLI он экономит больше, чем стоит, — я не люблю глобальные состояния, но жертвовать архитектурой ради пятисот строк утилиты не готов, и zap.L() с ReplaceGlobals в main — разумный компромисс с условием: как только файл растёт, зависимость уходит в конструкторы.

Чек-лист: включать ли глобал#

  • main-пакет или CLI-утилита без слоёв — можно, настройка в начале main.
  • Библиотека — никогда: принимает *zap.Logger или logr.Logger параметром.
  • Один вызов ReplaceGlobals на процесс: больше — повод пересмотреть дизайн.
  • Каждый вызов в тестах парный: ReplaceGlobals и defer restore().
  • t.Parallel и глобал несовместимы: уберите параллельность или глобал.
  • Горутины стартуют только после ReplaceGlobals — иначе их записи потеряны.

restore: сигнатура и альтернативы#

restore — это func() без аргументов и результата: она возвращает те глобальные значения, которые были до подмены, включая заглушку, если глобал ещё не настраивали. Подмена идёт парами — L и S меняются одновременно, рассинхрона не бывает. Если цель была не в подмене логгера, а в смене уровня, глобал трогать не нужно: атомарный уровень из урока про динамические уровни меняет порог на живом процессе без перевязки зависимостей. Подмена нужна там, где меняется само ядро: тест ставит наблюдателя, CI-скрипт перевключает файловый вывод. Правило разделения: уровень — через AtomicLevel, ядро — через ReplaceGlobals, и путать их не стоит.

Проверка: что возвращает ReplaceGlobals

Что возвращает zap.ReplaceGlobals(logger)?

Соберите настройку глобала

Восстановите вызовы: подмена, возврат, чтение глобала.

func main() { logger, _ := zap.NewProduction() restore := zap.(logger) defer () zap.().Info("started", zap.Int("port", 8080)) }

Соберите порядок настройки

Расставьте строки main по порядку: от сборки логгера до старта сервера.

Строки

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

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

Карточки: глобальный логгер

Термин

zap.ReplaceGlobals

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

Подменяет глобальные L и S на логгер и его sugar-версию; возвращает restore для отката.

1 / 5

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

Соседняя проблема — код, который не должен зависеть даже от факта наличия zap, — решается интерфейсом: мост logr и zapr разбирается в следующем уроке, и там подмена в тестах становится ещё проще. Дисциплина тестов глобала опирается на observer из урока про zaptest и observer. Карта всего курса — на странице курса zap; дальше по плану — эксплуатация и итоговый проект, где глобал, наконец, заменяется явной передачей.

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

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

Вывод

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

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

  • До ReplaceGlobals zap.L() — no-op: вызовы безопасны, записи исчезают.
  • ReplaceGlobals меняет L и S разом и возвращает restore для отката.
  • Опасность — порядок, а не data race: init и горутины до подмены пишут в заглушку.
  • В тестах ReplaceGlobals парный к defer restore(), с t.Parallel() глобал не дружит.
  • Библиотека глобал не трогает: принимает логгер параметром.
  • Паттерн зрелого проекта: настроили в main один раз, дальше передаём явно.
  • Уровень меняет AtomicLevel, ядро — ReplaceGlobals: разные инструменты.

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

Добавьте в маленький проект глобальный логгер: сборка через пресет, один вызов ReplaceGlobals в main с defer restore(). Напишите тест с observer, который проверяет запись из zap.L(), и убедитесь, что без restore() соседний тест ломается. Затем переведите один пакет с zap.L() на явную передачу логгера через конструктор и сравните, где проверки стали проще. После этого соберите predict-сценарий урока в тест: переменная, снятая до подмены, и zap.L() после — обе пишут Info, и одна запись теряется.

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

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

Насколько безопасна подмена глобала из нескольких горутин?

Внутри zap мьютекс — data race не будет, и это часто вводит в соблазн. Безопасна такая подмена по смыслу только на старте или в тестах: на живом трафике она меняет поведение всех пакетов разом, а это операция уровня main, не рантайма. Чужие горутины о подмене не уведомляются: кто успел снять zap.L() до неё, остался со старым значением.

Нужно ли вызывать restore() в main?

Практического эффекта нет: restore вернёт заглушку перед самым выходом из процесса. defer там — привычка и образец для тестов, где откат обязателен и без него ломаются соседние проверки.

Что делать с чужой библиотекой, которая пишет только в zap.L()?

Настроить глобал в main — это её единственный канал, и Named-потомков она не увидит: глобал один. Рабочие пути: договориться о настройке глобала в main, взять форк с параметром-логгером или вынести критичные события из библиотеки в свой код.

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

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

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

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

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

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

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

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

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