Содержание урока · 12 разделов
- Что решает Viper: один ключ — пять источников
- Пять источников и один порядок: как Viper раскладывает приоритеты
- Конфигурация в Go до Viper: как было
- Get и типизированные Get*: приведение типов
- Методы чтения Viper: что вернут при неудаче
- SetDefault: слой, который есть всегда
- Когда Viper не нужна
- Признаки, что без Viper уже не обойтись
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Релиз вашей утилиты вышел, и через неделю приходит первое пожелание: сделайте, чтобы порт настраивался без флага, у нас он всегда один и тот же. Вы добавляете чтение файла — руками, через encoding/yaml. Через месяц второе пожелание: в CI порт приходит переменной окружения. Вы добавляете os.LookupEnv с дефолтом из файла. Через квартал третье: у части команд порт другой, и хочется перекрыть его флагом. И вот у вас три источника, свой порядок перекрытий, самописная проверка типов и баг, где пустая строка из окружения отключала конфиг. Всё это — велосипед с квадратными колёсами, и Viper изобрела его за вас, только с круглыми: один реестр настроек, куда источники складываются в порядке приоритета, и один способ спросить значение.
Что решает Viper: один ключ — пять источников#
Viper написана Стивом Франсией — тем же человеком, что и Cobra, — и с 2014 года живёт рядом с ней в github.com/spf13/viper. Идея простая: приложение спрашивает значение по строковому ключу — viper.GetInt("server.port") — а откуда оно взялось, спрашивающего не волнует. Источников пять: значения, заданные кодом через viper.Set; флаги, привязанные через BindPFlag; переменные окружения; конфиг-файл; и дефолты через SetDefault. Viper сама хранит порядок силы: Set сильнее флага, флаг сильнее окружения, окружение сильнее файла, файл сильнее дефолта. Ваш код про настройки — одна строка чтения, а не лестница из LookupEnv и парсеров. С Cobra они ходят парой в kubectl и Hugo, и весь остаток курса мы будем собирать эту связку по кусочкам: сегодня — реестр и типы, в следующих уроках — файлы и окружение.
Пять источников и один порядок: как Viper раскладывает приоритеты#
Ключевую таблицу Viper стоит выучить наизусть — порядок силы источников. Сверху вниз: viper.Set — значение, заданное кодом во время работы; затем флаги, привязанные BindPFlag; затем переменные окружения; затем конфиг-файл; в самом низу — SetDefault. Когда вы спрашиваете GetInt, Viper проходит список сверху вниз и отдаёт первый найденный слой. Из этого следуют практические привычки: дефолты задаём сразу и много — их всегда перекрывает что-то живое; viper.Set используем осторожно — он сильнее пользователя, и если где-то в коде спрятан Set для порта, флаг бесполезен; спор флаг против окружения решается в десятой функции. Для отладки реестра пригодятся два метода-обзора: viper.AllKeys() возвращает список всех известных ключей, viper.AllSettings() — всю карту значений разом; их удобно печатать по флагу --verbose, пока лестница приоритетов не въелась в память. Всё устройство — один отсортированный список источников внутри библиотеки; никакой магии, только дисциплина порядка.
Конфигурация в Go до Viper: как было#
Полезно помнить, против чего Viper бунтовала. В стандартной библиотеке Go нет ни одного пакета конфигурации: flag отвечает за флаги, остальное — на вас. Типичный проект выглядел лестницей: os.LookupEnv для окружения, encoding/json или самописный парсер для файла, flag для терминала — и в каждом месте свои нулевые значения и свои ошибки. О порядке перекрытий обычно забывали: выигрывал тот источник, который проверялся последним. Viper не открыла ничего нового: она закрепила порядок, дала один API и стандартизировала ответы на четыре вопроса — где искать файл, что делать, когда его нет, как приводить типы и кто кого перекрывает. Ценность не в объёме библиотеки, а в том, что порядок больше не предмет обсуждения в каждой команде.
Первый реестр: SetDefault и типизированные Get
GOpackage main
import (
"fmt"
"github.com/spf13/viper"
)
func main() {
viper.SetDefault("server.port", 8080)
viper.SetDefault("verbose", false)
viper.SetDefault("tags", []string{"go", "cli"})
// Get возвращает any — приведение на вашей совести
fmt.Println(viper.Get("server.port"))
// Типизированные Get* приводят тип сами
fmt.Println(viper.GetInt("server.port"))
fmt.Println(viper.GetString("server.port"))
fmt.Println(viper.GetBool("verbose"))
fmt.Println(viper.GetStringSlice("tags"))
}SetDefault кладёт значения в самый нижний слой реестра. Get возвращает interface{} — без приведения почти бесполезен, поэтому в коде почти всегда зовут типизированные варианты: GetInt, GetString, GetBool, GetStringSlice. Запускать пример нужно локально: go mod init и go get github.com/spf13/viper, песочница сайта сторонних пакетов не исполняет.
Get и типизированные Get*: приведение типов#
Внутри приведением занимается маленький пакет spf13/cast, и ведёт он себя свободнее, чем вы ожидаете. GetInt("8080") по строковому значению вернёт число, GetString(8080) — строку: приведение работает в обе стороны. GetDuration понимает и «5s», и число наносекунд; GetStringSlice развернёт и список, и одиночное значение. Плата за свободу — молчание при неудаче: если значение привести нельзя, Get* возвращает нулевое значение своего типа и ни одной ошибки. Ноль вместо порта, пустая строка вместо пути, false вместо режима — и программа продолжает жить с тихо испорченной настройкой. Отсюда правило: значения, от которых зависит запуск, проверяйте через IsSet и собственную валидацию — cast отвечает за удобство, а не за корректность. И ещё нюанс: viper.Get по несуществующему ключу вернёт nil, а GetInt по нему — 0; отличить «настройки нет» от «настройка ноль» Get не умеет, для этого существует IsSet.
Методы чтения Viper: что вернут при неудаче#
| Метод | Вернёт | Если значение не приводится |
|---|---|---|
| Get(key) | any — как лежит | nil, если ключа нет |
| GetString | строку | пустую строку |
| GetInt | целое число | 0 |
| GetBool | логическое | false |
| GetDuration | time.Duration | 0 |
| GetStringSlice | срез строк | nil |
cast.ToBool понимает как истину только то, что понимает strconv.ParseBool: true, TRUE, True, 1, t, T. Русское «да», «вкл» и английское «enable» для него — false, и никакой ошибки вы не получите. Я наступал на это в связке с анкетами: пользователь писал «Включено», и фича молча выключалась. Критичные булевы настройки читайте осознанно и валидируйте отдельно.
SetDefault: слой, который есть всегда#
Дефолты в Viper живут в коде, и это осознанное решение: SetDefault — одновременно и настройка, и документация, ведь ключи с описаниями лежат в одном месте рядом с main. Вызывать SetDefault можно сколько угодно раз и по любым ключам; точка в имени — вложенность: server.port означает секцию server с полем port, и в конфиг-файле следующего урока она же превратится в вложенную структуру YAML. Вызывайте дефолты до чтения конфига — технически порядок не важен, но один блок SetDefault в начале initConfig читается как оглавление настроек приложения. По отношению к паттерну binding из шестого урока здесь есть дублирование: дефолты и в структуре, и в Viper. Пока допустимо; в десятой функции Unmarshal соберёт весь реестр в структуру одним вызовом, и двойной учёт исчезнет.
Блок дефолтов как оглавление приложения
GOpackage cmd
import "github.com/spf13/viper"
func init() {
viper.SetDefault("server.host", "127.0.0.1")
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.timeouts.read", "5s")
viper.SetDefault("server.timeouts.write", "10s")
viper.SetDefault("storage.path", "$HOME/.notes")
viper.SetDefault("storage.max_backups", 3)
viper.SetDefault("verbose", false)
}Точки в ключах образуют дерево: server.timeouts.read — второй уровень вложенности, и в YAML это будет секция в секции. GetDuration("server.timeouts.read") приведёт строку «5s» к time.Duration. Строки с $HOME Viper не разворачивает сама — это делает ваш код через os.UserHomeDir при использовании значения. И пересматривайте дефолты при релизах: настройка, появившаяся с дефолтом в 0.4, через год читается как часть контракта утилиты — менять её молча опаснее, чем добавить новую.
GetDuration и IsSet: приятные мелочи
GOpackage main
import (
"fmt"
"os"
"github.com/spf13/viper"
)
func main() {
viper.SetDefault("timeout", "30s")
viper.SetDefault("retries", 3)
d := viper.GetDuration("timeout")
fmt.Println("таймаут:", d)
if !viper.IsSet("license_key") {
fmt.Fprintln(os.Stderr, "лицензионный ключ не задан")
os.Exit(1)
}
fmt.Println("повторов:", viper.GetInt("retries"))
}GetDuration приводит и «30s», и «500ms», и число наносекунд: строки времени — самый приятный тип в cast. IsSet отвечает на вопрос «задан ли ключ хоть чем-то» — им отличают отсутствующую настройку от нулевой; здесь отсутствие ключа лицензии — законный повод упасть. Код требует локального Go с viper в go.mod: песочница сайта сторонние пакеты не исполняет. IsSet видит и окружение при включённом AutomaticEnv: переменная считается заданным ключом наравне с файлом.
Когда Viper не нужна#
Теперь честная часть. Если у утилиты один источник настроек — флаги из шестого урока, — Viper не нужна: паттерн binding закрывает вопрос, а лишняя зависимость в go.mod — это не только мегабайты, но и магия приоритетов, которую придётся объяснять каждому новому человеку в команде. История зависимостей у Viper тяжёлая: долгие годы она тянула клиентские библиотеки etcd и Consul для remote-бэкендов, в свежих версиях их спрятали за build-теги, но библиотека по-прежнему заметно толще flag. Мой критерий такой: появился второй источник настроек — файл или переменные окружения, — Viper окупается в первый же час; пока источник один, она окупается никогда. Спорное мнение: некоторые тянут Viper ради Get-ключей даже в маленьких утилитах — единообразие, говорят. Не соглашусь: GetString("port") вместо cfg.Port — это плюс строка, плюс кавычки, минус проверка типов компилятором. Останьтесь на структурах.
Признаки, что без Viper уже не обойтись#
- Пользователи просят конфиг-файл: настройки меняются чаще, чем запуски
- В CI утилита должна читаться из переменных окружения без правки скриптов
- В коде появилось три if по os.LookupEnv с разными дефолтами
- Порядок перекрытий «флаг, файл, env» обсуждается в чате, а не описан в коде
- Список настроек перевалил за десяток и живёт в разных файлах
- Два продукта компании хотят читать один конфиг разными утилитами
Проверка: приведение типов
В реестре лежит строка «да». Что вернёт viper.GetBool("flag")?
Проверка: когда брать Viper
В какой ситуации Viper оправдана?
Что напечатает программа
Прочитайте код и выберите, что он выведет:
package main
import (
"fmt"
"github.com/spf13/viper"
)
func main() {
viper.SetDefault("port", "8080")
viper.SetDefault("limit", 10)
fmt.Println(viper.GetInt("port"))
fmt.Println(viper.GetString("limit"))
}Восстановите работу с реестром
Впишите метод дефолта и два типизированных метода чтения.
Соберите минимальную программу с Viper
Расставьте строки от объявления пакета до закрывающей скобки main.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: Viper
Что дальше#
Реестр работает, но пока в нём только дефолты. Следующий урок, конфиг-файлы: YAML, TOML и JSON, научит Viper читать файл с диска и обрабатывать ConfigFileNotFoundError — ситуацию первого запуска, когда файла ещё нет. После файлов пойдут переменные окружения через AutomaticEnv — тот самый слой, который делает утилиту дружелюбной к CI. Обзор всего курса со списком уроков — на странице Cobra. Код урока, как всегда, требует локального Go с viper в go.mod: соберите, запустите, поменяйте дефолт на строку и посмотрите, как GetInt молча его проглотит.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Viper — реестр настроек с приоритетами: Set, флаги, env, файл, дефолты — от автора Cobra.
- Чтение — по строковому ключу: Get возвращает any, типизированные Get* приводят типы через cast.
- Приведение молчит: не привёлся — получишь нулевое значение без ошибки; критичное валидируй сам.
- GetBool понимает только true, TRUE, True, 1, t, T — «да» и «enable» превращаются в false.
- SetDefault — самый слабый слой и заодно оглавление настроек приложения.
- Один источник настроек — Viper не нужна: структуры с биндингом хватит.
- Второй источник (файл или env) — точка окупаемости Viper в первый час.
Домашнее задание
Подключите Viper к утилите заметок: go get github.com/spf13/viper, блок SetDefault с ключами server.port и storage.path, чтение через GetInt и GetString. Затем поменяйте дефолт порта на строку «8080» и проверьте, что GetInt всё равно вернёт число, а GetBool на «да» молча вернёт false. Финальный шаг: напечатайте viper.AllKeys() после инициализации и убедитесь, что оба ваших ключа в списке — так реестр становится отладочным инструментом, а не чёрным ящиком.
Личные заметкиПусто
Вопросы и ответы по уроку
Что такое Viper в Go?
Библиотека конфигурации от автора Cobra: единый реестр, куда складываются дефолты, конфиг-файлы, переменные окружения, флаги и значения из кода — с фиксированным порядком приоритета. Приложение читает значения по строковым ключам, не заботясь об источнике.
Viper или стандартный подход со структурами: что выбрать?
Если настройки приходят только из флагов — структуры с биндингом из предыдущего урока: меньше зависимостей и есть проверка типов. Как только появляется конфиг-файл или переменные окружения, Viper берёт расстановку приоритетов на себя и окупается.
Почему GetBool молча возвращает false?
Приведение типов в Viper выполняет пакет cast: для bool он понимает только true, TRUE, True, 1, t, T, а на остальных строках возвращает false без ошибки. Поэтому критичные значения проверяют через IsSet и собственную валидацию.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
- Конфиг-файлы: YAML, TOML и JSON
- Переменные окружения: AutomaticEnv
- Приоритеты конфигурации: флаг, env, файл
- Справка: кастомизация help и usage
- Флаги и структура конфига: паттерн binding
- Флаги: локальные и персистентные
- Аргументы командной строки и валидация Args
- Команды и подкоманды: дерево AddCommand
- Все курсы по Go
Следующая ступень учебного пути
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