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

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

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

Урок 9 из 25 ~18 мин +20 XP

Приоритеты источников: кто кого перекрывает

Приоритеты viper зашиты лестницей: Set сильнее флага, флаг сильнее env, env сильнее файла, файл сильнее дефолта. Слои по одному собирались в прошлых уроках — теперь сложим официальную лестницу целиком, разберём ловушку флага из деплоя и закроем порядок конфигурации go тестом на viper.New().

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

Вторник, вечер, прод. Инженер правит host базы в config.yaml, перезапускает сервис — и тот уходит в старый хост, как будто файла не существует. Конфиг syntactically безупречен, ключ на месте, в логах чисто. Через три часа находится виновник: в docker-compose.yml от эксперимента полугодовой давности осталась переменная APP_DATABASE_HOST, а окружение в viper перекрывает файл — не баг, а документированный порядок. Первые восемь уроков каждый слой лестницы разбирался по одному: файл, дефолты, окружение с префиксами, флаги. Теперь слои собираются в одну картину: официальный порядок из документации, ловушка на стыке флага и деплоя, тест, который держит лестницу под замком. И честно про практику: примеры этого урока требуют файлов, переменных и флагов.

Официальный порядок viper: шесть слоёв#

Документация viper фиксирует порядок слияния источников одной строкой на слой: explicit call to Set, затем флаги, затем переменные окружения, затем конфиг-файлы, затем внешние key/value-хранилища и в самом низу дефолты. Каждый пункт перекрывает всё, что ниже, и порядок один на все ключи реестра — менять его нельзя и не нужно. За каждым слоем стоит конкретный вызов из предыдущих уроков: Set из первого, BindPFlag из восьмого, AutomaticEnv и BindEnv из шестого и седьмого, ReadInConfig из второго, SetDefault из пятого. KV-хранилище — единственный слой, к которому мы ещё не подходили: etcd, Consul и Firestore через remote-провайдер, и в типичном сервисе он не нужен. Нюанс, без которого лестница читается неверно: «флаги» в списке означают переданные флаги — viper проверяет flag.Changed при каждом чтении, непереданный флаг из конкуренции выпадает. Всё, что важно про этот слой, разобрано в уроке про BindPFlag; слепая зона окружения — в уроке про AutomaticEnv.

Шесть слоёв в одной программе

GO
viper.SetDefault("server.port", 7070) // слой 6: дефолт
viper.SetConfigFile("config.yaml")    // слой 4 готовится к чтению
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()                  // слой 3: окружение
if err := viper.ReadInConfig(); err != nil { // слой 4: файл
        log.Fatalf("конфиг: %v", err)
}
viper.BindPFlag("server.port", rootCmd.Flags().Lookup("port")) // слой 2
viper.Set("server.port", 6000)        // слой 1: самый сильный

// config.yaml задаёт port: 9000, в окружении SERVER_PORT=9090,
// сервис запущен с --port 7000
fmt.Println(viper.GetInt("server.port")) // 6000

Шесть слоёв перечислены подряд — но порядок вызовов ничего не решает: силы слоёв фиксированы, и Set на последней строке всё равно перекрывает всех. Переставьте его выше ReadInConfig — результат тот же: реестр сверяет слои в момент Get, а не в момент вызова. Пощёлкать источниками вживую — флагом против файла, переменной против дефолта — можно только локальным Go: соберите бинарник и меняйте по одному слою.

server.port на каждом слое: как читать лестницу#

Одна и та же команда Get собрала шесть кандидатов на значение server.port: 6000 от Set, 7000 от переданного флага, 9090 из переменной SERVER_PORT, 9000 из config.yaml, значение из KV-хранилища и 7070 от SetDefault. Победил верхний слой — остальные не «усредняются» и не «пересматриваются», а игнорируются целиком: реестр не смешивает значения, он выбирает один. Отсюда практическое чтение лестницы: фраза «файл перекрыт» означает, что значение файла по этому ключу не видит никто; вернуть файлу силу можно только снятием слоя выше, а не добавлением ему веса снизу. Шестой слой — внешние хранилища — вписан документацией между файлом и дефолтами: etcd или Consul, подключённые remote-провайдером, отдают конфиг кластеру сервисов. Слой законный, но редкий: отдельная интеграция и отдельная точка отказа; здесь он упомянут для полноты, потому что лестница из документации насчитывает ровно шесть пунктов.

Лестница для server.port: слой, вызов, значение#

СлойВызов или источникserver.portКогда побеждает
1. Setviper.Set("server.port", 6000)6000программно; сильнее всех, порядок вызовов не важен
2. ФлагBindPFlag, ./app --port 70007000флаг передан пользователем: Changed=true
3. envAutomaticEnv, SERVER_PORT=90909090переменная задана в деплое
4. Конфиг-файлReadInConfig, port: 90009000база проекта: ключ есть в файле
5. KV-хранилищеremote-провайдер: etcd, Consulключ в хранилищецентрализованные конфиги кластера
6. ДефолтSetDefault 70707070выше никто ключ не объявил
Хвостдефолт непереданного флага8080последний шанс после всех слоёв

Ловушка, которую восьмой урок разобрал по частям, в общей лестнице встаёт на своё место. Слой «флаг» означает переданный флаг: viper читает flag.Changed в момент каждого Get. Скрипт деплоя вызывает ./app --port 8080 — значение совпадает с дефолтом флага, пользователь «ничего не менял», но Changed равно true: флаговый слой заморожен выше файла на каждом запуске, и config.yaml по server.port мёртв навсегда, без единой ошибки в логах. Вторая половина ловушки — непереданный флаг: он из конкуренции выпадает, а его дефолт достаётся последним шансом, ниже SetDefault и KV-хранилища, отчего опечатанный ключ в yaml тихо заменяется флаговым значением. Тот же порядок со стороны CLI разобран в уроке про приоритеты конфигурации Cobra — там флаг, env и файл встречаются без реестра. Правило остаётся: флаги — для разовых перекрытий человеком, всё постоянное живёт в файле и окружении.

Тест лестницы на viper.New()

GO
func TestLadder_envOverFileOverDefault(t *testing.T) {
        v := viper.New()
        v.SetConfigType("yaml")
        yaml := "server:\n  port: 9000\n"
        if err := v.ReadConfig(strings.NewReader(yaml)); err != nil {
                t.Fatal(err)
        }
        v.SetDefault("server.port", 7070)

        t.Setenv("SERVER_PORT", "9090")
        v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
        v.AutomaticEnv()

        if got := v.GetInt("server.port"); got != 9090 {
                t.Fatalf("env должен перекрыть файл: got %d", got)
        }

        v.Set("server.port", 6000)
        if got := v.GetInt("server.port"); got != 6000 {
                t.Fatalf("Set должен перекрыть env: got %d", got)
        }
}

Тест читает YAML из строки — файловая система не нужна, поведение детерминировано. viper.New() выдаёт отдельный экземпляр реестра: глобальный viper не задействован, состояние между тестами не перемешивается. t.Setenv ставит переменную на время теста и снимает её после; функция доступна с Go 1.17. Три утверждения фиксируют три ступени: env сильнее файла, файл сильнее дефолта, Set сильнее env. Запуск — go test локальным Go: viper не входит в стандартную библиотеку, и песочница сайта его не умеет.

Лестницу надо тестировать#

Порядок приоритетов деградирует тихо: коллега добавил BindPFlags(rootCmd.PersistentFlags()) на весь набор флагов — и слой флага расползся на ключи, для которых флагов никто не задумывал; тест, проверявший один ключ, остался зелёным. Реестр не отвечает, откуда пришло значение: в viper нет API «источник этого значения», и сломанную лестницу видно только по поведению. Поэтому лестница — контракт, и контракт тестируют. Схема: viper.New() даёт чистый реестр, YAML подаётся строкой через ReadConfig, переменная ставится t.Setenv, каждое ожидание — отдельное утверждение: файл сильнее дефолта, env сильнее файла, Set сильнее env, непереданный флаг не мешает. Двадцать строк теста ловят ровно те сюрпризы, что врезка выше нашла за три часа в проде: сместился слой — тест краснеет в pull request, а не в дежурство.

Одна сборка реестра для main и тестов

GO
func buildRegistry(v *viper.Viper) error {
        v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
        v.AutomaticEnv()
        v.SetConfigFile("config.yaml")
        if err := v.ReadInConfig(); err != nil {
                return err
        }
        v.SetDefault("log.level", "info")
        return nil
}

func main() {
        v := viper.New()
        if err := buildRegistry(v); err != nil {
                log.Fatal(err)
        }
        fmt.Println(v.GetInt("server.port"))
}

Единственная точка сборки: replacer, окружение, файл и дефолты в одной функции. main создаёт чистый экземпляр viper.New() и передаёт параметром — глобальный реестр не участвует. Тест из блока выше заменяет ручную сборку на тот же buildRegistry и проверяет настоящую лестницу, а не тестовую копию. Файл config.yaml читается настоящей файловой системой — и в этом смысл проверки.

Спор: глобальный viper или viper.New() с DI#

Вокруг viper.New() идёт ровный многолетний спор. Первый лагерь: глобальный viper короче — viper.Get по всему коду, ноль церемоний, и в утилите с двумя ключами это честный выбор. Второй: v := viper.New() в main, передача реестра через зависимости — потребители не прячут глобальное состояние, тесты не дерутся за общий реестр. Моя позиция с границей: в сервисе, где конфиг читают один-два пакета, я оставляю глобальный viper ради простоты, но лестницу приоритетов всё равно гоняю тестом на viper.New() — новый экземпляр стоит вызова функции, а ловит больше, чем линтер. Как только потребителей становится три и больше или появляется второй конфиг-файл, перехожу на New с передачей целиком: глобальное состояние к этому моменту стоит дороже удобства. Команды с развитой культурой DI зовут глобальный viper легаси с первого дня — и в их контексте это рабочее мнение: спор здесь про контекст, а не про истину.

Гигиена приоритетов#

  • Печатайте значения ключей на старте через Get, а не AllSettings: дамп слеп к окружению, а Get сходят по лестнице честно.
  • Собирайте реестр одной функцией для main и тестов: лестница, собранная дважды разными руками, — две разные лестницы.
  • Флаги — слой разовых перекрытий человеком: скрипт, передающий флаг на каждом запуске, навсегда замораживает слой выше файла.
  • Дефолты объявляйте SetDefault, а не «дефолтом флага»: последний шанс ниже всех, и IsSet его не видит.
  • После любого изменения сборки реестра обновляйте тест лестницы: порядок без теста деградирует молча.
  • KV-хранилище подключайте осознанно: слой между файлом и дефолтом тянет интеграцию с etcd или Consul и отдельную точку отказа.

Проверка: env против файла

config.yaml задаёт server.port: 9000, SetDefault ставит 7070, replacer настроен, AutomaticEnv включена, в окружении SERVER_PORT=9090. Что вернёт viper.GetInt("server.port")?

Проверка: флаг с дефолтным значением

Скрипт деплоя запускает ./app --port 8080, где 8080 — значение по умолчанию флага, привязанного BindPFlag к ключу server.port. В config.yaml записан port: 9000. Что вернёт GetInt?

Что выведет программа?

Прочитайте код и выберите, что он выведет:

v := viper.New()
v.SetConfigType("yaml")
v.ReadConfig(strings.NewReader("server:\n  port: 9000\n"))
v.SetDefault("server.port", 7070)
v.Set("server.port", 6000)

fmt.Println(v.GetInt("server.port"))

Соберите тест лестницы

Допишите создание чистого реестра, чтение YAML из строки, установку переменной окружения в тесте и чтение значения.

v := viper.() v.SetConfigType("yaml") v.(strings.NewReader("server:\n port: 9000\n")) t.("SERVER_PORT", "9090") v.AutomaticEnv() if got := v.("server.port"); got != 9090 { t.Fatalf("env должен перекрыть файл: got %d", got) }

Соберите сборку реестра по шагам

Расставьте строки: чистый экземпляр, replacer, окружение, чтение файла и вывод значения.

Строки

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

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

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

Термин

Лестница приоритетов

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

Документированный порядок viper по убыванию: Set, флаги, окружение, конфиг-файлы, key/value-хранилища, дефолты. Один порядок на все ключи, менять его нельзя.

1 / 6

Что дальше#

Лестница собрана целиком: шесть слоёв из документации, флаговый хвост на своём месте, порядок под тестом на viper.New(). Последний большой приём чтения — Unmarshal: конфиг целиком в структуру одним вызовом, вместо десятка GetString по коду; он же встретит нас новой ловушкой на стыке с окружением. Домой, к маршруту по урокам и темам, ведёт страница курса viper. Домашняя практика из реальной жизни: соберите buildRegistry, накройте тестом и сломайте слой намеренно — уберите AutomaticEnv и посмотрите, как тест ловит деградацию раньше дежурства.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Документированный порядок viper: Set → флаг → env → конфиг-файл → key/value-хранилище → дефолт; один на все ключи, менять нельзя.
  • «Флаг» в лестнице — переданный: viper проверяет Changed при каждом Get; непереданный флаг из конкуренции выпадает.
  • Дефолт непереданного флага — последний шанс ниже SetDefault; IsSet его не видит.
  • Флаг, который скрипт деплоя передаёт на каждом запуске, замораживает слой выше файла — даже если значение равно дефолту флага.
  • Реестр не отвечает, откуда пришло значение; сломанная лестница видна только по поведению — поэтому её закрывают тестом.
  • viper.New() даёт чистый реестр: тест с ReadConfig из строки и t.Setenv детерминирован и не трогает глобальное состояние.
  • Собирайте реестр одной функцией для main и тестов: две сборки — две разные лестницы.

Домашнее задание

Вынесите сборку реестра в функцию buildRegistry и напишите TestLadder на viper.New(): YAML из строки задаёт порт 9000, дефолт — 7070, через t.Setenv приходит 9090. Три утверждения: файл сильнее дефолта, env сильнее файла, Set сильнее env. Прогон — локальным go test.

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

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

Можно ли узнать, из какого слоя viper взял значение?

Прямого API нет: реестр не хранит происхождение значений. Рабочий способ — изоляция по одному слою: уберите переменную, закомментируйте ключ в файле, снимите привязку флага и смотрите на Get. В тесте на viper.New() слои включаются по одному — эффект виден сразу.

Порядок слоёв можно настроить?

Нет: лестница зашита в viper, и она одна на все ключи. Собственный порядок даёт слой над реестром — свой пакет config, где вы сами решаете, что перекрывает что; в курсе ему посвящён отдельный урок. Перестановка слоёв в самом viper не предусмотрена — решение авторов.

KV-хранилище — это Redis?

Нет: remote-провайдер viper работает с etcd, Consul и Firestore — список из документации. Redis в нём нет. Слой стоит между конфиг-файлом и дефолтами и подключается импортом провайдера и AddRemoteProvider; для большинства сервисов это лишняя интеграция, файл и окружение покрывают всё.

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

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

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

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

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

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

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