Содержание урока · 10 разделов
- Отладка конфигурации go: порядок допроса
- ReadInConfig, опечатки и ключи с точкой
- AllKeys, AllSettings и IsSet: что реально в реестре
- Env-слой и приоритеты: значение перекрыто выше
- Симптом → причина → лечение
- Финальный чек-лист: ключ не читается
- Спор: дамп реестра на старте или чистые логи
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Отладка конфигурации 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 реально знает
GOkeys := 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 печатает значения, поэтому в логи его не пускаем — к секретам вернёмся ниже.
Историю про APP_DATABASE_HOST из compose-файла мы разбирали в уроке про приоритеты: сервис читал не тот источник, и поиск шёл три часа. Теперь случай укладывается в чек-лист за минуты: AutomaticEnv включён — grep по коду; префикс правильный — SetEnvPrefix("APP") даёт APP_DATABASE_HOST, а "APP_" даёт APP__ с двойным подчёркиванием, viper добавляет черту сам; replacer превращает database.host в DATABASE_HOST, а не DATABASE_HOST_; переменная вообще доезжает в контейнер — docker exec env. Четыре вопроса по порядку — и три часа сжимаются в десять минут.
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) или непустое значение |
Финальный чек-лист: ключ не читается#
- Проверьте ошибку ReadInConfig: подчёркивание вместо if err != nil — источник тишины номер один.
- Напечатайте AllKeys: нет ключа — ищите опечатку или непрочитанный источник; есть — идите дальше по списку.
- Уточните ключ с точкой: это вложенность, а не плоское имя; сверяйтесь с уроком про вложенные ключи.
- Допросите env-слой: AutomaticEnv, префикс без хвостового подчёркивания, replacer, docker exec env.
- Поднимитесь по лестнице приоритетов: Set, env, файл, дефолты — кто перекрыл ваше значение.
- Сверьте тип вызова с типом в источнике: 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())Соберите каркас допроса
Допишите диагностику: собрать все ключи реестра, проверить обязательный и достать значение.
Соберите дамп реестра
Расставьте строки диагностики по порядку: включить env-слой, собрать ключи, отсортировать и напечатать каждый с проверкой.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Диагностическая печать без секретов
GOfunc 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 из урока про обёртку: диагностике не нужен доступ к глобальному реестру.
Сервис начал падать в проде, дежурный добавил в main строку log.Println(v.AllSettings()) — классика диагностики. Причину починили, строку удалили, а пароль базы остался: реестр печатается целиком, сборщик логов разнёс его по индексам, и через полгода пароль нашёлся поиском в Loki. Об ошибках этого сорта мы говорили в уроке про секреты: дамп конфигурации — самый короткий путь секрета наружу. Диагностика без утечки: AllKeys, маскирующий dumpKeys из предыдущего раздела и точечные IsSet закрывают те же вопросы без печати значений.
Карточки: диагностика реестра
Спор: дамп реестра на старте или чистые логи#
Моё правило, которое в командах любят оспаривать: на старте сервиса печатать не весь конфиг, а ровно 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 — и пройдите чек-лист от симптома до причины, записывая каждый шаг.
Личные заметкиПусто
Вопросы и ответы по уроку
AllSettings или AllKeys — что печатать при инциденте?
AllKeys: список имён без значений безопасен и уже отвечает на главный вопрос — доставлен ли ключ вообще. AllSettings показывает значения, но печатает секреты целиком, поэтому в проде его место только за маскировкой. Связка AllKeys плюс точечные IsSet закрывает большинство инцидентов.
Почему viper не пишет в лог о пропущенном ключе?
Так устроен контракт Get-функций: отсутствие ключа — не ошибка, а нулевое значение, и библиотека молчит, чтобы пустые настройки не роняли сервис. Платой становится диагностика: пропуск ловится AllKeys, IsSet и проверками на старте, а не сообщениями из рантайма.
Ключ точно есть в AllKeys, IsSet true, а значение не то, что в файле. Что это?
Классическое перекрытие: выше по лестнице стоит Set или env-переменная, а файл остался ниже. Проверьте AllSettings против ожиданий и поднимитесь по лестнице из урока про приоритеты: Set, затем env, затем файл, дефолты в самом низу.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context