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

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

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

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

Viper: слежение за конфигом и секреты

viper watchconfig включает живой reload настроек без перезапуска: агент, висящий на сервере месяц, подхватывает новый интервал опроса из YAML за секунду. Вторая половина урока — обратная сторона гибкости: что нельзя класть в конфиг, почему секреты в конфиге go живут в переменных окружения и как не засветить токен в логах.

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

Утилита-агент опрашивает API каждые тридцать секунд и уже месяц живёт на сервере. Прод оказался под нагрузкой, и админ просит увеличить интервал и затихомошить логи. Перезапускать агента нельзя: он держит локальное состояние очереди, а просто перечитать файл он не умеет — конфиг читается один раз при старте, как мы и разложили в уроке про структуру. Выход есть: fsnotify умеет сообщать о изменениях файлов, а viper обёртывает это в два вызова — WatchConfig и OnConfigChange. Заодно разберём тему, о которой при слове «конфиг» забывают чаще всего: в YAML нельзя класть секреты, и это не паранойя, а следствие того, как устроен git и как живут логи.

WatchConfig: что происходит под капотом#

viper.WatchConfig() заводит вотчер библиотеки fsnotify — на Linux это inotify, на macOS kqueue и FSEvents — и подписывает его на файл конфига. Обязательный порядок вызовов: сначала SetConfigFile или SetConfigName с ReadInConfig, и только затем WatchConfig — наблюдать за ещё не открытым файлом нечего, и viper промолчит, а вы узнаете об этом по стагнации настроек. Важная деталь реализации: viper следит не только за самим файлом, но и за каталогом, поэтому переживает атомарную замену — vim, sed -i и большинство редакторов пишут временный файл и переименовывают его на место старого, меняя inode. Обработчик изменения регистрируется через viper.OnConfigChange и получает fsnotify.Event с путём файла — дальше всё зависит от того, как вы храните значения.

Базовая связка watch и события

GO
func loadConfig(path string) (*config.Config, error) {
        v := viper.New()
        v.SetConfigFile(path)
        if err := v.ReadInConfig(); err != nil {
                return nil, err
        }
        v.WatchConfig()
        v.OnConfigChange(func(e fsnotify.Event) {
                log.Printf("конфиг изменён: %s", e.Name)
        })
        var cfg config.Config
        if err := v.Unmarshal(&cfg); err != nil {
                return nil, err
        }
        return &cfg, nil
}

viper.New() вместо глобального синглтона — изоляция для тестов из урока про структуру: каждый тест строит свой экземпляр. WatchConfig стоит после ReadInConfig — за нечитанным файлом следить нечего. Unmarshal здесь делает снимок на старте: о том, как значения доходят до работающего агента, — следующий блок.

Свежие значения: viper.Get против снимка#

После перезагрузки конфига вопрос «откуда брать актуальное» встаёт сразу, и ответов два. Первый: вызовы viper.GetString и viper.GetInt всегда возвращают свежее значение — viper перечитал файл и держит его в памяти. Второй: структура, полученная Unmarshal'ом при старте, — снимок момента, она не обновится сама. Отсюда два рабочих стиля: либо спрашивать viper по требованию в каждой команде — для CLI с пятью настройками это честно и просто; либо поддерживать структуру конфига в актуальном состоянии — и вот тут появляется проблема параллельности, потому что писать структуру из обработчика событий, пока её читают команды, нельзя без синхронизации. Я выбираю стиль по объёму: пара значений — viper.Get, десяток полей, используемых в горячем цикле, — снимок под локом. Отдельная ловушка: горутины, стартовавшие до перезагрузки, держат старое значение в локальной переменной, и reload их не коснётся — снимок надо брать в момент использования, а не в момент старта горутины.

Живой reload: горутина-подписчик#

Обработчик OnConfigChange выполняется в горутине fsnotify, а не в вашей: заснув там, вы заморозите приём следующих событий, и конфиг формально «наблюдается», но не обновляется. Правило: обработчик лёгкий — перечитать структуру, опубликовать её, уведомить подписчиков — и наружу. Публикация снимка решается cheapest way: atomic.Value, где Store кладёт новую структуру, Load её отдаёт, и никто никого не блокирует. Для значений, требующих реакции, — интервал опроса, лимиты — заводится горутина-подписчик: она читает свежий снимок и пересоздаёт тикер с новым периодом. Последний нюанс — дребезг: некоторые редакторы сохраняют файл дважды за секунду, и на каждое сохранение придёт своё событие; если перечитывание дорогое, ставьте дебаунс таймером на двести-триста миллисекунд. Собирается он из time.AfterFunc за три строки: обработчик отменяет предыдущий таймер и заводит новый, а перечитывание стартует, только когда пауза прошла без новых событий — журнал перестаёт пестреть повторами.

Публикация снимка через atomic.Value

GO
var current atomic.Value // хранит config.Config

v.OnConfigChange(func(e fsnotify.Event) {
        var fresh config.Config
        if err := v.Unmarshal(&fresh); err != nil {
                log.Printf("конфиг битый, держим старый: %v", err)
                return
        }
        current.Store(fresh)
        changes <- fresh // уведомить горутину-подписчика
})

func CurrentConfig() config.Config {
        return current.Load().(config.Config)
}

atomic.Value — самый дешёвый способ опубликовать новый снимок: читатели не блокируются, писатель один. Битый YAML не роняет агента — свежий снимок не проходит валидацию, и продолжает работать старый. Канал changes получает то же значение: горутина-подписчик пересоздаёт тикер, не касаясь общих полей напрямую. Проверка ошибки Unmarshal перед Store — не украшение: без неё в current уехали бы нулевые поля, и агент «успешно» сбросил бы себе настройки.

Проверка: где живёт обработчик

В какой горутине выполняется функция, переданная в OnConfigChange?

Ловушки live-reload#

  • WatchConfig до ReadInConfig — следить не за чем, настройки молча стагнируют
  • Файла нет при старте — вотчерить нечего, ConfigFileNotFoundError из урока про файлы конфигов
  • Двойное сохранение редактором — два события за секунду, нужен дебаунс
  • Блокирующий OnConfigChange — замораживает приём событий в горутине fsnotify
  • Unmarshal без синхронизации — гонка с читателями, берите atomic.Value или мьютекс
  • WatchConfig при отсутствии файла молча не ставит вотчер — после первого успешного ReadInConfig вызовите повторно

Секреты в конфиге go: что нельзя коммитить#

Теперь вторая половина урока. YAML со строкой token: eyJhbGc... — это токен, попавший в git-историю навсегда: файл удалят, а строки в истории останутся, и git log достанет их через год. Правило слоёв из урока про приоритеты конфигурации уже даёт правильный ответ: секреты читаются из переменных окружения, поверх файла, а в YAML живут только несекретные значения — интервалы, адреса, уровни логов. Для локальной разработки заводится .env рядом с проектом: он грузится шеллом или direnv и стоит в .gitignore с первого дня; в репозиторий кладут .env.example со списком ключей и пустыми значениями. Связка viper.AutomaticEnv и SetEnvKeyReplacer из урока про переменные окружения делает это прозрачным для кода: команды продолжают читать viper.GetString, источник меняется без правок.

Куда класть настройку: этажи реестра#

НастройкаМестоПочему
Интервал опроса, лимитыYAML-конфигменяется админом без перезапуска — WatchConfig подхватит
API-токен, пароль базыПеременная окруженияне попадает в git и в сгенерированные файлы
Путь хранилища на конкретной машине.env рядом с проектомлокальная специфика, в репозиторий не едет
Флаг одной командыpflag с BindPFlagпользователь решает на каждый запуск

Env поверх файла

GO
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// viper.Get("api.token") прочитает API_TOKEN,
// даже если в YAML этого ключа нет

Replacer переводит точки ключа в подчёркивания имени переменной: api.token ищется как API_TOKEN. Приоритет из урока про приоритеты — env выше файла: ключ, заданный окружением, перекрывает YAML-значение. Сам viper .env-файлы не читает — их грузит шелл, direnv или systemd-юнит на сервере. Заметьте: viper сам поднимает имя переменной в верхний регистр — реплейсер заботится только о точках и дефисах, капс писать не нужно.

Минимизация данных и дисциплина логов#

Секреты — частный случай общего правила: собирайте и храните только то, что нужно работе. Это и требование GDPR-минимума — персональные данные обрабатываются по необходимости, а не «на всякий случай» — и просто меньшая площадь атаки. Вторая линия обороны — логи: секрет не должен появиться ни в fmt.Fprintf, ни в структурных полях zap, куда он часто просачивается через небрежное поле вроде token: myToken. Маскируйте по умолчанию: печатайте первые два символа и звёздочки, а лучше вообще булево поле hasToken. Если организация всё же требует держать секреты в файлах — шифруйте их age на диске или берите из vault-агента при старте, но это уже вопросы инфраструктуры, а не библиотеки конфигурации, и за рамки курса они выходят.

Проверка: место для токена

Где держать API-токен в продакшене?

Что вернёт viper.Get

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

// config.yaml содержит: level: debug
os.Setenv("TASKS_LEVEL", "info")

v := viper.New()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
v.SetConfigFile("config.yaml")
_ = v.ReadInConfig()

fmt.Println(v.GetString("level"))

Соберите включение наблюдения

Впишите вызов вотчера и тип аргумента обработчика.

v.() v.OnConfigChange(func(e fsnotify.) { v.Unmarshal(&fresh) })

Соберите инициализацию по порядку

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

Строки

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

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

Карточки: watch и секреты

Термин

WatchConfig

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

Включает наблюдение за файлом конфига через fsnotify; вызвать после ReadInConfig. Viper следит и за каталогом — переживает атомарную замену редактором.

1 / 6

Что дальше#

Конфиг живой, секреты под контролем — впереди финиш: итоговый проект соберёт менеджер задач со всей цепочки курса, от фабрик до версий. Основа уже готова: чтение файлов разбиралось в уроке про конфиг-файлы, лестница источников — в уроке про приоритеты. Если захочется углубить дисциплину логов и маскирование секретов в структурных полях — откройте курс zap: там эта тема разобрана на уровне библиотеки. Обзор всего курса — на странице Cobra. И оговорка: fsnotify-события видны только живому процессу — запустите утилиту локально, измените YAML и убедитесь, что журнал отреагировал; песочница файловую систему не исполняет.

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

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

Вывод

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

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

  • WatchConfig включает живой reload конфига; вызвать строго после ReadInConfig.
  • OnConfigChange выполняется в горутине fsnotify: не блокировать, дребезг гасить дебаунсом.
  • viper.Get всегда актуален; Unmarshal-структура — снимок, обновляйте её через atomic.Value.
  • Битый YAML на reload не роняет процесс: не распарсился — продолжаем на старой конфигурации.
  • Секреты — из переменных окружения поверх файла: AutomaticEnv с SetEnvKeyReplacer, в YAML только несекретное.
  • .env — вне git, в репозитории .env.example без значений; токен из истории вычищают и ротируют.
  • Секрет не попадает в логи: маскирование по умолчанию, минимум собираемых данных.

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

Включите WatchConfig и OnConfigChange в своей утилите: на событие перечитывайте конфиг в atomic.Value и печатайте журнал без секретов. Соберите локально, запустите, измените YAML редактором и убедитесь, что новые значения подхватились без перезапуска. Затем уберите ключ из файла и передайте его через переменную окружения — проверьте, что env победил файл. И сломайте YAML во время работы: битый файл не должен ронять процесс — агент обязан продолжить на старом снимке и оставить сообщение о битом конфиге в журнале.

Личные заметкиПусто
Хранятся только в вашем браузере (Cobra · Viper: слежение за конфигом и секреты)

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

Как перечитывать конфиг без перезапуска CLI?

viper.WatchConfig() после ReadInConfig плюс viper.OnConfigChange(func(e fsnotify.Event){...}): viper следит за файлом и каталогом через fsnotify. Свежие значения отдаёт viper.Get сразу, а структуру из Unmarshal обновляйте в обработчике под мьютексом или публикуйте через atomic.Value.

Почему обработчик OnConfigChange нельзя блокировать?

Он вызывается в горутине fsnotify, которая рассылает события: уснувший обработчик остановит приём следующих изменений. Тяжёлую работу — перечитать структуру, пересоздать тикер, уведомить подписчиков — выносите в канал и отдельную горутину, оставляя колбэку только публикацию снимка.

Куда положить API-токен для CLI?

В переменную окружения поверх файла: viper.AutomaticEnv с SetEnvKeyReplacer прочитает API_TOKEN для ключа api.token, даже если в YAML его нет. Файлы с секретами в репозиторий не попадают: .env вне git, а просочившийся в историю токен требует ротации и вычистки filter-repo.

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

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

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

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

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

Bubble Tea25 уроков~10 чпрактика25 уроков уже доступны

Bubble Tea: TUI-приложения в терминале

Интерактивный курс Bubble Tea — TUI-фреймворка на Go: Elm-архитектура, bubbles, Lip Gloss, мышь и клавиатура. 25 уроков с примерами и итоговым приложением.

  • Понимать Elm-архитектуру: Model, Update, View, Init
  • Обрабатывать клавиатуру и мышь в терминале
  • Писать асинхронные операции через tea.Cmd, Batch и Sequence
  • Использовать bubbles: list, textinput, textarea, table, viewport, progress, spinner