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

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

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

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

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

Viper — golang-библиотека конфигурации от автора Cobra: один API над дефолтами, файлами, переменными окружения и флагами. Разберём Get и типизированные Get*, SetDefault, приведение типов через cast и честно ответим, когда Viper не нужна совсем.

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

Релиз вашей утилиты вышел, и через неделю приходит первое пожелание: сделайте, чтобы порт настраивался без флага, у нас он всегда один и тот же. Вы добавляете чтение файла — руками, через 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

GO
package 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
GetDurationtime.Duration0
GetStringSliceсрез строкnil

SetDefault: слой, который есть всегда#

Дефолты в Viper живут в коде, и это осознанное решение: SetDefault — одновременно и настройка, и документация, ведь ключи с описаниями лежат в одном месте рядом с main. Вызывать SetDefault можно сколько угодно раз и по любым ключам; точка в имени — вложенность: server.port означает секцию server с полем port, и в конфиг-файле следующего урока она же превратится в вложенную структуру YAML. Вызывайте дефолты до чтения конфига — технически порядок не важен, но один блок SetDefault в начале initConfig читается как оглавление настроек приложения. По отношению к паттерну binding из шестого урока здесь есть дублирование: дефолты и в структуре, и в Viper. Пока допустимо; в десятой функции Unmarshal соберёт весь реестр в структуру одним вызовом, и двойной учёт исчезнет.

Блок дефолтов как оглавление приложения

GO
package 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: приятные мелочи

GO
package 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"))
}

Восстановите работу с реестром

Впишите метод дефолта и два типизированных метода чтения.

func main() { viper.("port", 8080) viper.("verbose", false) port := viper.("port") verbose := viper.GetBool("verbose") fmt.Println(port, verbose) }

Соберите минимальную программу с Viper

Расставьте строки от объявления пакета до закрывающей скобки main.

Строки

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

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

Карточки: Viper

Термин

Viper

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

Библиотека конфигурации (spf13, 2014): единый реестр настроек с приоритетами — Set, флаги, env, файл, дефолты. Сосед Cobra по репозиторию и автору.

1 / 5

Что дальше#

Реестр работает, но пока в нём только дефолты. Следующий урок, конфиг-файлы: 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() после инициализации и убедитесь, что оба ваших ключа в списке — так реестр становится отладочным инструментом, а не чёрным ящиком.

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

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

Что такое Viper в Go?

Библиотека конфигурации от автора Cobra: единый реестр, куда складываются дефолты, конфиг-файлы, переменные окружения, флаги и значения из кода — с фиксированным порядком приоритета. Приложение читает значения по строковым ключам, не заботясь об источнике.

Viper или стандартный подход со структурами: что выбрать?

Если настройки приходят только из флагов — структуры с биндингом из предыдущего урока: меньше зависимостей и есть проверка типов. Как только появляется конфиг-файл или переменные окружения, Viper берёт расстановку приоритетов на себя и окупается.

Почему GetBool молча возвращает false?

Приведение типов в Viper выполняет пакет cast: для bool он понимает только true, TRUE, True, 1, t, T, а на остальных строках возвращает false без ошибки. Поэтому критичные значения проверяют через IsSet и собственную валидацию.

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

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

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

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

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

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