Содержание урока · 11 разделов
- WatchConfig: что происходит под капотом
- Свежие значения: viper.Get против снимка
- Живой reload: горутина-подписчик
- Ловушки live-reload
- Секреты в конфиге go: что нельзя коммитить
- Куда класть настройку: этажи реестра
- Минимизация данных и дисциплина логов
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Утилита-агент опрашивает 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 и события
GOfunc 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
GOvar 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 поверх файла
GOv.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AutomaticEnv()
// viper.Get("api.token") прочитает API_TOKEN,
// даже если в YAML этого ключа нетReplacer переводит точки ключа в подчёркивания имени переменной: api.token ищется как API_TOKEN. Приоритет из урока про приоритеты — env выше файла: ключ, заданный окружением, перекрывает YAML-значение. Сам viper .env-файлы не читает — их грузит шелл, direnv или systemd-юнит на сервере. Заметьте: viper сам поднимает имя переменной в верхний регистр — реплейсер заботится только о точках и дефисах, капс писать не нужно.
SetEnvPrefix из урока про окружение чаще всего и пропускают: вы задаёте TASKS_TOKEN в CI, а viper ищет API_TOKEN — ключ api.token без префикса превращается ровно в него, и утилита молча читает пустую строку. Проверка на живом реестре: Get вернёт пусто, IsSet — false, и только печать AllKeys покажет, что слой вообще не подключён. Ставьте префикс рядом с AutomaticEnv одной строкой: v.SetEnvPrefix("tasks") — и ключи ищутся как TASKS_API_TOKEN.
Минимизация данных и дисциплина логов#
Секреты — частный случай общего правила: собирайте и храните только то, что нужно работе. Это и требование GDPR-минимума — персональные данные обрабатываются по необходимости, а не «на всякий случай» — и просто меньшая площадь атаки. Вторая линия обороны — логи: секрет не должен появиться ни в fmt.Fprintf, ни в структурных полях zap, куда он часто просачивается через небрежное поле вроде token: myToken. Маскируйте по умолчанию: печатайте первые два символа и звёздочки, а лучше вообще булево поле hasToken. Если организация всё же требует держать секреты в файлах — шифруйте их age на диске или берите из vault-агента при старте, но это уже вопросы инфраструктуры, а не библиотеки конфигурации, и за рамки курса они выходят.
Сценарий, который случается чаще, чем кажется: токен добавили в config.yaml для локального теста, закоммитили вместе с остальным, заметили через неделю. Удаление файла следующим коммитом проблему не решает — git-история отдаёт старые версии по любому хешу. Порядок лечения: сначала ротация токена у провайдера — это главная операция, потому что скомпрометированный секрет живёт у чужих людей; затем вычищение истории через git filter-repo или BFG и force-push; затем .gitignore и перенос значения в переменную окружения. Отдельное спорное мнение: я не кладу в репозиторий даже фейковые .env с заведомо не-настоящими ключами — привычка коммитить такие файлы однажды стрельнёт настоящим ключом, и цена привычки слишком высока.
Проверка: место для токена
Где держать 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"))Соберите включение наблюдения
Впишите вызов вотчера и тип аргумента обработчика.
Соберите инициализацию по порядку
Расставьте вызовы от создания экземпляра до включения слежения.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: watch и секреты
Что дальше#
Конфиг живой, секреты под контролем — впереди финиш: итоговый проект соберёт менеджер задач со всей цепочки курса, от фабрик до версий. Основа уже готова: чтение файлов разбиралось в уроке про конфиг-файлы, лестница источников — в уроке про приоритеты. Если захочется углубить дисциплину логов и маскирование секретов в структурных полях — откройте курс 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 во время работы: битый файл не должен ронять процесс — агент обязан продолжить на старом снимке и оставить сообщение о битом конфиге в журнале.
Личные заметкиПусто
Вопросы и ответы по уроку
Как перечитывать конфиг без перезапуска 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 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