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

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

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

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

Диагностика: почему viper не видит ключ

Viper не видит ключ — жалоба, на которую я отвечаю чаще, чем на все вопросы про форматы вместе взятые: значение вроде задано, а GetString возвращает пустоту или дефолт, который никто не объявлял. Соберём порядок допроса реестра: AllKeys, AllSettings и IsSet, семь типовых причин от пропущенного ReadInConfig до перекрытого приоритетами значения — и таблицу «симптом → лечение».

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

Отладка конфигурации go: порядок допроса#

Отладка конфигурации go сводится к допросу реестра в порядке лестницы источников, и нарушать этот порядок — верный способ потратить вечер. Сначала отвечаем: файл вообще прочитан? Ошибка ReadInConfig, проглоченная подчёркиванием, — причина номер один, и закрывается она первой. Затем: ключ существует в реестре? AllKeys показывает всё, что viper знает, — если ключа там нет, дальше проверять нечего, ищите опечатку или непрочитанный источник. Потом: значение перекрыто? Set, env, файл и дефолты стоят на лестнице в строгом порядке, и ваше значение могло просто лежать ниже чужого. И последнее: совпадает ли тип? GetString по списку вернёт пустую строку молча, без единой жалобы в лог. История из урока про приоритеты — три часа на APP_DATABASE_HOST из compose — прошла ровно по этим ступеням. Здесь тот же разбор собирается в чек-лист, который работает при любой причине.

ReadInConfig, опечатки и ключи с точкой#

Начните с файла. if err := v.ReadInConfig(); err != nil — и если err уезжает в подчёркивание, viper продолжает жить с пустым реестром, а все Get молча отдают нули: это самый дорогой тип тишины в библиотеке — снаружи выглядит как «сервис работает». Вторая ступень — опечатка в ключе: GetString("database.hsot") вернёт пустую строку и не моргнёт, компилятор строкам не помощник. Ловится простым приёмом: напечатайте AllKeys и ищите глазами близнеца вашего ключа. Третья — ключ с точкой: viper трактует точку как разделитель вложенности, и ключ data.base в YAML — это секция data с полем base, а не плоское имя. Если источник отдаёт ключи с точками в имени — чужой JSON, метрики, переменные вида APP.DB.HOST, — используйте SetEnvKeyReplacer или берите значение с учётом вложенности, как разобрано в уроке про вложенные ключи. Отдельно про дефолты: SetDefault объявляет ключ в AllKeys — если ключ виден только благодаря дефолту, ни файл, ни env его так и не доставили.

Дамп реестра: что viper реально знает

GO
keys := v.AllKeys()
sort.Strings(keys)

fmt.Println("ключей в реестре:", len(keys))
for _, k := range keys {
    fmt.Printf("  %-28s set=%v
", k, v.IsSet(k))
}

// ключа нет в списке? значит, его нет ни в файле,
// ни в env, ни в дефолтах — источник не доставлен

AllKeys возвращает все ключи, известные реестру из всех прочитанных источников, — уже в нижнем регистре, как viper их хранит. Сортировка нужна для воспроизводимого вывода: сравнивайте два дампа между окружениями, а не бегайте глазами по случайному порядку карт. IsSet в цикле отделяет «ключ объявлен, но значения нет» от «ключа нет вовсе» — это разные причины и разное лечение. Скопируйте каркас в свой проект и прогоните локальным Go: пять минут, которые обычно экономят вечер.

AllKeys, AllSettings и IsSet: что реально в реестре#

Три инструмента отвечают на три разных вопроса, и путать их дорого. AllKeys — «что реестр знает»: список имён из файлов, env-привязок, дефолтов и remote-источников; нет ключа в списке — проблема доставки, а не значения. AllSettings — «что реестр отдаст»: карта значений, собранная по Get для каждого известного ключа, то есть с применением всей лестницы приоритетов; расхождение этой карты с ожиданиями говорит уже о перекрытии, а не о пропаже. IsSet — «есть ли значение у конкретного ключа»: проверка существования по той же лестнице, по которой читает Get; ставьте её рядом с обязательными настройками: пусть пустота всплывает на старте, а не на первом коннекте. Рабочая связка для инцидента: len(v.AllKeys()) == 0 — не прочитан ни один источник; ключ есть, а значение не то — поднимайтесь по лестнице. И помните: AllSettings печатает значения, поэтому в логи его не пускаем — к секретам вернёмся ниже.

Env-слой и приоритеты: значение перекрыто выше#

Env-слой — источник самых дорогих сюрпризов, потому что его видно хуже всех. Первое: AutomaticEnv выключен, а BindEnv для ключа никто не написал — переменная стоит в окружении, viper о ней не знает, и Get честно проваливается к файлу, как разобрано в уроке про окружение. Второе: префикс не тот — APP_ вместо APP даёт двойное подчёркивание в имени переменной. Третье: пустая переменная по умолчанию считается не заданной — viper провалится к файлу или дефолту, и AllowEmptyEnv(true) включается осознанно, если пустота имеет смысл. Всё это — верхние ступени лестницы из урока про приоритеты источников: Set выше всего, за ним env, потом файл, в самом низу дефолты. Практический вывод: если Get вернул не то, что в файле, — поднимайтесь по лестнице и ищите, кто стоит выше.

Две последние причины — самые безобидные на вид. Регистр: viper хранит все ключи в нижнем регистре и читает их без учёта регистра — Get("DATABASE.HOST"), Get("Database.Host") и Get("database.host") вернут одно и то же, так что «не совпал регистр» как причина пропажи не существует; зато в дампе AllKeys вы увидите нижний регистр, и это норма, а не бага. Типы — другое дело: Get-функции возвращают нулевое значение своего типа, если приведение не удалось, и не жалуются. GetString по списку из YAML — пустая строка, GetInt по строке "abc" — ноль, GetBool по невнятному значению — false, и никакой ошибки ни в рантайме, ни в логе. Лечение одно: брать функцию под тип источника — GetStringSlice для списков, GetIntSlice для числовых, Get для неопределённых случаев с type assertion по месту.

Тип не тот: GetString по списку

GO
// config.yaml: servers: ["app-1", "app-2"]

fmt.Println(v.GetString("servers"))      // "" — приведение не удалось, тишина
fmt.Println(v.GetStringSlice("servers")) // [app-1 app-2] — то, что нужно

port := v.GetInt("server.port")          // в YAML строка "9090"
fmt.Println(port)                        // 9090: cast вытянул число из строки

GetString по списку возвращает пустую строку: приведение среза к string невозможно, и cast отдаёт нулевое значение без ошибки — сервис продолжает жить с пустотой. GetStringSlice приводит каждый элемент и собирает срез. Заметьте несимметрию: GetInt из строки "9090" вытянет число — парсить строку в int cast умеет, а вот срез в строку превращать не во что. Прогоните пример локальным Go на реальном YAML: поведение нулевых значений запоминается только руками.

Симптом → причина → лечение#

СимптомЧастая причинаЛечение
Все Get возвращают нулиошибка ReadInConfig проглочена, реестр пустпроверять err, падать на старте
Один ключ пуст, остальные на местеопечатка в ключеAllKeys глазами, константы для имён
Ключ из чужого JSON не читаетсяточка в имени трактуется как вложенностьreplacer или обращение по вложенности
Переменная из compose не виднанет AutomaticEnv/BindEnv или не тот префиксgrep SetEnvPrefix, docker exec env
В Get не то, что в файлезначение перекрыто Set или env выше по лестницеAllSettings против ожиданий
GetString по списку вернул пустотутип не тот: Get отдаёт нулевое значениеGetStringSlice, Get + type assertion
Get вернул дефолт вместо env-значенияпустая переменная считается не заданнойAllowEmptyEnv(true) или непустое значение

Финальный чек-лист: ключ не читается#

  1. Проверьте ошибку ReadInConfig: подчёркивание вместо if err != nil — источник тишины номер один.
  2. Напечатайте AllKeys: нет ключа — ищите опечатку или непрочитанный источник; есть — идите дальше по списку.
  3. Уточните ключ с точкой: это вложенность, а не плоское имя; сверяйтесь с уроком про вложенные ключи.
  4. Допросите env-слой: AutomaticEnv, префикс без хвостового подчёркивания, replacer, docker exec env.
  5. Поднимитесь по лестнице приоритетов: Set, env, файл, дефолты — кто перекрыл ваше значение.
  6. Сверьте тип вызова с типом в источнике: GetString на слайсе даёт пустую строку без единой жалобы.

Проверка: ключа нет в AllKeys

AllKeys() не содержит ключ, который вы лично видели в config.yaml. Какое первое подозрение?

Проверка: тип значения

В YAML объявлено retries: [1, 2, 3]. Что вернёт GetString("retries")?

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

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

// configs/config.yaml:
// Database:
//   Host: "db.internal"

v := viper.New()
v.SetConfigName("config")
v.AddConfigPath("./configs")
if err := v.ReadInConfig(); err != nil {
    log.Fatal(err)
}

fmt.Println(v.AllKeys())

Соберите каркас допроса

Допишите диагностику: собрать все ключи реестра, проверить обязательный и достать значение.

keys := v.() sort.Strings(keys) if !v.("database.host") { log.Fatal("database.host не задан") } host := v.("database.host") fmt.Println(host)

Соберите дамп реестра

Расставьте строки диагностики по порядку: включить env-слой, собрать ключи, отсортировать и напечатать каждый с проверкой.

Строки

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

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

Диагностическая печать без секретов

GO
func dumpKeys(v *viper.Viper) {
    keys := v.AllKeys()
    sort.Strings(keys)

    fmt.Println("config keys:")
    for _, k := range keys {
        if strings.Contains(k, "password") ||
            strings.Contains(k, "secret") ||
            strings.Contains(k, "token") {
            fmt.Printf("  %s = ***
", k)
            continue
        }
        fmt.Printf("  %s = %v
", k, v.Get(k))
    }
}

Версия дампа, которую можно показывать на старте: имена ключей всегда, значения — только для нечувствительных. Фильтр по password, secret и token грубоват, но ловит большую часть беды; параноидальный вариант — печатать только AllKeys без значений вообще. Функция принимает экземпляр viper.Viper, поэтому работает и с пакетом config из урока про обёртку: диагностике не нужен доступ к глобальному реестру.

Карточки: диагностика реестра

Термин

AllKeys

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

Все ключи, известные реестру из прочитанных источников: файлы, env-привязки, дефолты, remote. Возвращаются в нижнем регистре. Нет ключа в AllKeys — проблема доставки, а не значения.

1 / 6

Спор: дамп реестра на старте или чистые логи#

Моё правило, которое в командах любят оспаривать: на старте сервиса печатать не весь конфиг, а ровно AllKeys с IsSet — и никакого AllSettings в проде. Аргумент за полный дамп понятен: инцидент расследуется по логу старта, видно всё сразу. Но цена проявляется однажды и сразу с секретами: реестр печатается целиком, включая password и token, а логи живут дольше любого деплоя — история из callout выше не выдумана, такие тикеты я видел. Компромисс, который работает: в dev-окружении полный дамп разрешён и даже полезен, в prod — только имена ключей, значения по точечному запросу и маскировка чувствительных имён, как в dumpKeys. Граница у спора такая: если у вас централизованный сбор логов с поиском, полный дамп в проде запрещён — это не вкусовщина, а гигиена; локальный запуск без сборщика может позволить себе что угодно.

Диагностика собрана в систему: порядок допроса от чтения файла до типов, три инструмента реестра, таблица симптомов и чек-лист, который можно повесить рядом с мониторингом. Это последний технический урок про сам viper — дальше разговор про альтернативы: когда конфиг из env дешевле реестра и чем хороши koanf со стандартной библиотекой. Общий маршрут по темам — на странице курса viper. Прямо сейчас добавьте dumpKeys в свой сервис и посмотрите на вывод на реальном конфиге — если он удивит, урок уже окупился.

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

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

Вывод

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

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

  • Viper не видит ключ молча: Get-функции возвращают нулевое значение, поэтому диагностика идёт по чек-листу, а не по логам.
  • Первая причина — непрочитанный файл: проглоченная ошибка ReadInConfig оставляет реестр пустым при живом сервисе.
  • AllKeys — что реестр знает, AllSettings — что он отдаёт, IsSet — есть ли значение у конкретного ключа.
  • Env-слой проверяется по шагам: AutomaticEnv, префикс без хвостового подчёркивания, replacer, непустая переменная.
  • Точка в ключе — вложенность, регистр не важен, GetString на слайсе даёт пустую строку: тип вызова подбирается под источник.
  • Полный дамп реестра в проде — короткий путь секрета в логи: печатайте имена и маскируйте чувствительные значения.
  • Чек-лист «симптом → причина → лечение» превращает вечер поиска в десять минут — проверено на истории с APP_DATABASE_HOST.

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

Соберите dumpKeys из урока в свой сервис и прогоните на реальном конфиге: список ключей, IsSet по обязательным, тип вызова под тип значения. Затем намеренно сломайте один источник — переименуйте ключ в YAML или уберите AutomaticEnv — и пройдите чек-лист от симптома до причины, записывая каждый шаг.

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

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

AllSettings или AllKeys — что печатать при инциденте?

AllKeys: список имён без значений безопасен и уже отвечает на главный вопрос — доставлен ли ключ вообще. AllSettings показывает значения, но печатает секреты целиком, поэтому в проде его место только за маскировкой. Связка AllKeys плюс точечные IsSet закрывает большинство инцидентов.

Почему viper не пишет в лог о пропущенном ключе?

Так устроен контракт Get-функций: отсутствие ключа — не ошибка, а нулевое значение, и библиотека молчит, чтобы пустые настройки не роняли сервис. Платой становится диагностика: пропуск ловится AllKeys, IsSet и проверками на старте, а не сообщениями из рантайма.

Ключ точно есть в AllKeys, IsSet true, а значение не то, что в файле. Что это?

Классическое перекрытие: выше по лестнице стоит Set или env-переменная, а файл остался ниже. Проверьте AllSettings против ожиданий и поднимитесь по лестнице из урока про приоритеты: Set, затем env, затем файл, дефолты в самом низу.

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

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

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

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

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

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

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