Содержание урока · 10 разделов
- Три окружения: dev, staging, prod
- Константы и простыни флагов не масштабируются
- Библиотека viper: один реестр и лестница приоритетов
- Источники, которые viper складывает в реестр
- Лестница приоритетов viper
- Обзор API: пять вызовов, с которых начинается проект
- Карта курса: от первого YAML до итогового проекта
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Пятница, вечер, релиз. Сборка уходит на staging и падает: порт веб-сервера в тестовом контуре занят, база живёт на другой машине, ключ платёжного API в стенде свой. Вы открываете main.go, правите три константы, коммитите, пересобираете. В понедельник то же самое в обратную сторону. Через месяц таких пятниц в репозитории три ветки с разной прошивкой настроек, новые разработчики собирают проект по инструкции из чата, а история git помнит пароль, который кто-то «на минуту» вписал в код. Это не кривые руки — это отсутствие слоя конфигурации: программа не умеет получать настройки снаружи, и каждое изменение среды превращается в правку кода. С этого места начинается разговор про viper — библиотеку, которая принимает настройки из файлов, окружения и флагов и отдаёт их программе по одному ключу.
Три окружения: dev, staging, prod#
У учебного примера одно окружение — ноутбук. У боевого сервиса их минимум три, и разница между ними не в версии бинарника, а в настройках. На dev порт 8080, база локальная, логи болтливые. На staging намерения те же, но стенд общий: порт чаще всего 80, база в докере с другим паролем, логи умеренные. На prod — свой хост базы, настоящий секретный ключ, строгие логи. Бинарник при этом один и тот же: собирать отдельную сборку под каждое окружение — путь к классическому «а на стенде работало». Значит, настройки должны приходить снаружи, в момент запуска. Откуда именно — вопрос стратегии: YAML-файл рядом с бинарником, переменные окружения из systemd или Kubernetes, флаги из командной строки. Правильный ответ почти всегда «всё сразу, с приоритетами»: файл задаёт основу, окружение перекрывает её в деплое, флаг — для разового запуска. Именно это и умеет viper.
Простыня флагов, с которой никто не справляется
GOvar (
port = flag.String("port", "8080", "порт HTTP-сервера")
dbHost = flag.String("db-host", "localhost", "хост базы")
dbPort = flag.String("db-port", "5432", "порт базы")
dbUser = flag.String("db-user", "app", "пользователь базы")
dbPass = flag.String("db-pass", "", "пароль базы")
apiKey = flag.String("api-key", "", "ключ платёжного API")
logLevel = flag.String("log-level", "info", "уровень логов")
permsFile = flag.String("perms-file", "", "файл прав доступа")
env = flag.String("env", "dev", "окружение")
)
flag.Parse()Девять флагов — и это минимум для крошечного сервиса. Запуск превращается в командную строку на три строки экрана, пароль светится в истории shell и в выводе ps, а при добавлении настройки надо править код и все скрипты деплоя сразу. Флаги хороши для управления поведением запуска — verbose, dry-run, путь к файлу, — а не для хранения всей конфигурации.
Константы и простыни флагов не масштабируются#
У проблемы трёх окружений два плохих решения, и оба встречаются в проде чаще, чем хотелось бы. Первое — константы и переменные пакета: const port = 8080. Меняется настройка — меняется код, пересборка, новый релиз ради смены пароля. Секреты при этом живут в git навсегда: удаление коммита не стирает его из истории и из чужих клонов. Второе — флаги на всё: команда запуска превращается в простыню, которую невозможно прочитать, невозможно продублировать между машинами и опасно показать в логе — ps aux увидит пароль в открытом виде. Оба решения не отвечают и на вопрос приоритетов: если порт задан и флагом, и в файле, кто победит? Каждый проект отвечает по-своему — до первого инцидента. Viper делает этот выбор один раз за всех и одинаковый для всех ключей, о чём дальше.
Библиотека viper: один реестр и лестница приоритетов#
Viper — конфиг-библиотека Стива Франсии, автора Cobra и Hugo: живёт с 2014 года в github.com/spf13/viper и носит порядка 27 тысяч звёзд на GitHub — по данным репозитория на момент написания курса. Идея умещается в одно предложение: все источники настроек сходятся в один реестр, и программа спрашивает значение по строковому ключу, не зная, откуда оно пришло. viper.GetString("database.host") вернёт хост базы независимо от того, задали вы его в YAML, в переменной окружения или флагом. За этим стоит лестница приоритетов, зашитая в библиотеку: явный Set сильнее флага, флаг сильнее окружения, окружение сильнее конфиг-файла, файл сильнее key/value-хранилища, в самом низу — дефолты из SetDefault. Справедливости ради: в курсе Cobra viper появлялся как эпизод — один урок на знакомство. Здесь он главный герой: двадцать пять уроков от первого YAML до production-схемы с валидацией и горячей перезагрузкой.
Источники, которые viper складывает в реестр#
- Явный Set — значение, заданное кодом во время работы; самый сильный слой, обращаться осторожно.
- Флаги командной строки — привязка BindPFlag к Cobra или stdlib flag.
- Переменные окружения — AutomaticEnv и BindEnv, стандартный канал деплоя.
- Конфиг-файл — YAML, JSON, TOML, HCL, INI, Java-properties и env-файлы.
- Key/value-хранилища — etcd и Consul через remote-провайдер.
- SetDefault — дефолты, нижний слой, без которого программа не поднимется без файла.
Лестница приоритетов viper#
| Слой | Чем задаётся | Кому нужен |
|---|---|---|
| 1. Set | viper.Set("server.port", 9000) в коде | тестам и аварийным переключениям — осторожно |
| 2. Флаг | флаг --port, привязанный BindPFlag | разовому запуску и CLI-утилитам |
| 3. Окружение | переменная SERVER_PORT | деплою: systemd, Docker, Kubernetes |
| 4. Конфиг-файл | config.yaml рядом с бинарником | базовой настройке проекта |
| 5. KV-хранилище | etcd или Consul | распределённым установкам |
| 6. Дефолт | viper.SetDefault("server.port", 8080) | запуску без единого внешнего источника |
Установка
GOgo mod init example.com/service
go get github.com/spf13/viperОбычный go get: viper не требует отдельного CLI, генераторов кода и обвязки. Импорт в коде — github.com/spf13/viper, API работает с глобальным реестром сразу, без конструктора. Сторонние пакеты исполняются только локальным Go: браузерная песочница сайта не тянет зависимости и не имеет ни сети, ни файловой системы, поэтому все примеры курса запускаются у вас на машине.
Обзор API: пять вызовов, с которых начинается проект#
Весь вход в библиотеку viper — горстка функций над глобальным реестром. SetDefault задаёт значение по умолчанию: viper.SetDefault("server.port", 8080) — и программа без файла всё равно поднимется. SetConfigFile называет конкретный файл конфигурации, ReadInConfig читает и разбирает его в реестр — пара, с которой начинается следующий урок про первый YAML. Get("server.port") возвращает значение как any: универсально, но без типа, поэтому в реальном коде зовут типизированных наследников — GetString, GetInt, GetBool, GetStringSlice, — которые приводят значение к нужному типу сами. Set кладёт значение поверх всех источников: это самый сильный слой, и о нём — предупреждение ниже. Всё остальное API — Unmarshal в структуру, WatchConfig для перезагрузки, BindPFlag для флагов — надстройки над этими пятью вызовами, и каждому найдётся отдельный урок.
Дефолты и первое чтение — скелет будущего сервиса
GOviper.SetDefault("server.port", 8080)
viper.SetDefault("database.host", "localhost")
viper.SetConfigFile("config.yaml")
if err := viper.ReadInConfig(); err != nil {
log.Printf("конфиг не прочитан: %v", err)
}
fmt.Println(viper.GetInt("server.port"))
fmt.Println(viper.GetString("database.host"))Скелет читается сверху вниз: два дефолта гарантируют работу без файла, SetConfigFile называет YAML, ReadInConfig грузит его в реестр, ошибка обрабатывается — здесь пока печатью в лог. Если файла нет, программа не падает: Get вернёт дефолтные 8080 и localhost. Полный разбор этой пары вызовов и их ошибок — следующий урок.
Заводите SetDefault для всех ключей до того, как появился первый конфиг-файл: дефолт документирует настройку, позволяет программе работать без файла и превращает «ключ не найден» в штатную работу. Мой ориентир: если настройка упоминается в README, у неё есть дефолт — пусть даже пустая строка. Проверить, задан ли ключ хоть в каком-то слое, можно через viper.IsSet.
Спрятанный в недрах кода viper.Set("server.port", 9000) перекрывает и флаг, и окружение, и файл: пользователь ставит порт через --port, а сервис всё равно слушает 9000. Такие Set появляются легко — «временно, для отладки» — и живут годами. Правило: Set — инструмент тестов и аварийных веток, а не основной способ задать настройку; основное место значения — файл, окружение или дефолт.
Сценарий «файла нет» — норма, а не авария, и viper оставляет решение за вами. ReadInConfig вернёт ошибку: если файл искали по путям через AddConfigPath, это ConfigFileNotFoundError с перечнем просмотренных локаций; если путь задан явно через SetConfigFile — обычная файловая ошибка вроде no such file or directory. Проигнорируете ошибку — Get начнёт отвечать дефолтами, и программа молча поедет на них: в dev это спасение, в проде источник недоумения «почему слушаем 8080, если в конфиге 9000». Поэтому реакция на ошибку — продуктовое решение: CLI-утилите достаточно предупредить и жить на дефолтах, боевому сервису честнее упасть на старте с внятным сообщением, чем подниматься с чужими настройками. Оба варианта пишутся в одну и ту же ветку if err != nil — разница только в вызове внутри. Запускать эти опыты нужно локально: у песочницы в браузере нет файловой системы, а файлы здесь — главный герой.
Проверка: кто кого перекрывает
Программа задаёт viper.SetDefault("server.port", 8080), в config.yaml написан port: 9000, а сервис запущен с флагом --port 7000, привязанным через BindPFlag. Что вернёт viper.GetInt("server.port")?
Проверка: Get и типизированные наследники
Чем viper.Get("server.port") отличается от viper.GetInt("server.port")?
Что напечатает программа?
Прочитайте код и выберите, что он выведет:
viper.SetDefault("server.port", 8080)
viper.Set("server.port", 9999)
viper.SetConfigFile("config.yaml")
// в файле: port: 9000
_ = viper.ReadInConfig()
fmt.Println(viper.GetInt("server.port"))Соберите скелет конфигурации
Допишите вызовы viper: дефолт, имя файла, чтение и типизированное чтение.
Соберите минимальную программу конфигурации
Расставьте строки программы: от объявления пакета до печати значения.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: реестр и приоритеты
Карта курса: от первого YAML до итогового проекта#
Курс ведёт от первого файла до production-схемы, и каждый урок добавляет один слой. Дальше по порядку: первый YAML через SetConfigFile и ReadInConfig, форматы от JSON до TOML — один API для всех, затем типизированные Get, дефолты, окружение и префиксы, связка с флагами Cobra и большая тема приоритетов источников. Во второй половине — Unmarshal в структуры, вложенные ключи, горячая перезагрузка WatchConfig, профили dev/staging/prod и секреты, которые не должны жить в git. Финал — итоговый проект: один бинарник читает файл, перекрывается окружением и флагами, валидирует значения на старте и перечитывает конфиг без перезапуска. Если база Go ещё шатается, начните с базового курса — viper опирается на структуры, ошибки и пакеты.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- У сервиса три и больше окружений при одном бинарнике — настройки обязаны приходить снаружи, в момент запуска.
- Константы ведут к пересборкам и секретам в git, простыни флагов — к нечитаемым командам запуска и паролям в выводе ps.
- Viper собирает источники в один реестр: программа спрашивает значение по ключу, не зная, откуда оно пришло.
- Лестница приоритетов: Set > флаг > окружение > конфиг-файл > key/value-хранилище > дефолт.
- Установка обычная: go mod init и go get github.com/spf13/viper — без CLI, генераторов и обвязки.
- База API: SetDefault, SetConfigFile и ReadInConfig, Get и типизированные GetString, GetInt, GetBool.
- viper.Set — самый сильный слой: инструмент тестов и аварийных веток, а не основной канал настройки.
Домашнее задание
Создайте пустой модуль, установите viper через go get github.com/spf13/viper и соберите скелет из урока: два SetDefault, SetConfigFile на несуществующий config.yaml и ReadInConfig с обработкой ошибки. Запустите без файла и убедитесь, что Get вернул дефолты, затем создайте config.yaml и посмотрите, как изменится ошибка. Запуск — локально: песочница файлов не имеет.
Личные заметкиПусто
Вопросы и ответы по уроку
Viper слишком тяжёлая для маленькой утилиты?
Для утилиты с двумя-тремя настройками хватит flag и os.LookupEnv — без зависимостей. Viper окупается, когда источников становится несколько: файл, окружение, флаги, дефолты — и им нужен порядок перекрытия. Граница простая: пока порядок источников помещается в голове, библиотеку можно отложить; как только появился второй контур деплоя — берите viper.
Кто автор viper и насколько библиотека зрелая?
Стив Франсия — автор Cobra и Hugo; viper живёт с 2014 года и входит в стандартный набор Go-инфраструктуры, порядка 27 тысяч звёзд на GitHub на момент написания курса. Зрелость видна в деталях: десятки форматов, работа с окружением, привязка флагов и порядок приоритетов, который не меняется годами.
Можно ли читать конфиг без файла вообще?
Да: дефолты через SetDefault плюс переменные окружения через AutomaticEnv покрывают контейнерные деплои, где конфиг-файл не принят. Реестру viper не нужен ни один источник: без файла и без окружения Get вернёт дефолт. Файл — самый удобный способ задать базовую конфигурацию, но не обязательный.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context