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

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

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

Урок 1 из 25 ~18 мин +50 XP

Знакомство с Viper: конфигурация без боли

Viper — golang-библиотека, после которой править константы перед каждой выкаткой больше не нужно: у сервиса три окружения, и в каждом свой порт, свой адрес базы и свой ключ API. Разберём, почему константы и простыни флагов перестают масштабироваться, что viper кладёт в один реестр и как выглядит её API — от SetDefault до ReadInConfig.

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

Пятница, вечер, релиз. Сборка уходит на staging и падает: порт веб-сервера в тестовом контуре занят, база живёт на другой машине, ключ платёжного API в стенде свой. Вы открываете main.go, правите три константы, коммитите, пересобираете. В понедельник то же самое в обратную сторону. Через месяц таких пятниц в репозитории три ветки с разной прошивкой настроек, новые разработчики собирают проект по инструкции из чата, а история git помнит пароль, который кто-то «на минуту» вписал в код. Это не кривые руки — это отсутствие слоя конфигурации: программа не умеет получать настройки снаружи, и каждое изменение среды превращается в правку кода. С этого места начинается разговор про viper — библиотеку, которая принимает настройки из файлов, окружения и флагов и отдаёт их программе по одному ключу.

Три окружения: dev, staging, prod#

У учебного примера одно окружение — ноутбук. У боевого сервиса их минимум три, и разница между ними не в версии бинарника, а в настройках. На dev порт 8080, база локальная, логи болтливые. На staging намерения те же, но стенд общий: порт чаще всего 80, база в докере с другим паролем, логи умеренные. На prod — свой хост базы, настоящий секретный ключ, строгие логи. Бинарник при этом один и тот же: собирать отдельную сборку под каждое окружение — путь к классическому «а на стенде работало». Значит, настройки должны приходить снаружи, в момент запуска. Откуда именно — вопрос стратегии: YAML-файл рядом с бинарником, переменные окружения из systemd или Kubernetes, флаги из командной строки. Правильный ответ почти всегда «всё сразу, с приоритетами»: файл задаёт основу, окружение перекрывает её в деплое, флаг — для разового запуска. Именно это и умеет viper.

Простыня флагов, с которой никто не справляется

GO
var (
        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. Setviper.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)запуску без единого внешнего источника

Установка

GO
go 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 для флагов — надстройки над этими пятью вызовами, и каждому найдётся отдельный урок.

Дефолты и первое чтение — скелет будущего сервиса

GO
viper.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. Полный разбор этой пары вызовов и их ошибок — следующий урок.

Сценарий «файла нет» — норма, а не авария, и 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: дефолт, имя файла, чтение и типизированное чтение.

viper.("server.port", 8080) viper.("config.yaml") if err := viper.(); err != nil { log.Fatal(err) } port := viper.("server.port") fmt.Println(port)

Соберите минимальную программу конфигурации

Расставьте строки программы: от объявления пакета до печати значения.

Строки

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

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

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

Термин

Viper

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

Конфиг-библиотека spf13, автора Cobra и Hugo; с 2014 года. Собирает файлы, окружение, флаги и дефолты в один реестр с порядком приоритетов. Установка: go get github.com/spf13/viper.

1 / 6

Карта курса: от первого 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 · Знакомство с Viper: конфигурация без боли)

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

Viper слишком тяжёлая для маленькой утилиты?

Для утилиты с двумя-тремя настройками хватит flag и os.LookupEnv — без зависимостей. Viper окупается, когда источников становится несколько: файл, окружение, флаги, дефолты — и им нужен порядок перекрытия. Граница простая: пока порядок источников помещается в голове, библиотеку можно отложить; как только появился второй контур деплоя — берите viper.

Кто автор viper и насколько библиотека зрелая?

Стив Франсия — автор Cobra и Hugo; viper живёт с 2014 года и входит в стандартный набор Go-инфраструктуры, порядка 27 тысяч звёзд на GitHub на момент написания курса. Зрелость видна в деталях: десятки форматов, работа с окружением, привязка флагов и порядок приоритетов, который не меняется годами.

Можно ли читать конфиг без файла вообще?

Да: дефолты через SetDefault плюс переменные окружения через AutomaticEnv покрывают контейнерные деплои, где конфиг-файл не принят. Реестру viper не нужен ни один источник: без файла и без окружения Get вернёт дефолт. Файл — самый удобный способ задать базовую конфигурацию, но не обязательный.

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

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

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

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

Chi: роутер для Go HTTP-сервисов

Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.

  • Строить маршруты chi: методы, параметры пути, wildcards
  • Писать middleware и понимать порядок их выполнения
  • Группировать маршруты: Route, Group и подроутеры
  • Прокидывать данные в хендлеры через context.Context