Содержание урока · 10 разделов
- Чтение yaml в Go: что лежит в config.yaml
- viper.SetConfigFile и viper.ReadInConfig: два вызова
- Ключи регистронезависимы, вложенность — через точку
- Ошибки ReadInConfig: файл не найден и файл сломан
- Три ошибки ReadInConfig и реакция на них
- Чек-лист первого запуска
- Диагностика и что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Скелет из первого урока компилируется, но настройки всё ещё живут в дефолтах. Сегодня появляется config.yaml: порт сервера, хост базы, таймауты — то, что меняется между окружениями без пересборки. Задача формулируется в одну строку: прочитать файл и достать значения. По пути всплывают детали, на которых новички теряют вечер: ключи в viper регистронезависимы, вложенность адресуется точкой, а ошибка чтения, пропущенная по невнимательности, не роняет программу — она оставляет её с пустым реестром, где GetString честно возвращает пустую строку. Разберём оба вызова построчно, посмотрим на три вида ошибок и на то, как правильно разводить их по разным реакциям.
Чтение yaml в Go: что лежит в config.yaml#
YAML — самый частый формат конфигов в мире Go: он же живёт в Kubernetes-манифестах, docker-compose и GitHub Actions, и писать конфиг сервиса в том же синтаксисе — решение без сюрпризов для ops. Формат читается как дерево: пары ключ: значение, вложенность задаётся отступом в два пробела, комментарии начинаются с решётки. Файл урока покрывает минимум сервиса — порт и хост базы — плюс вложенный блок таймаутов, чтобы к концу урока было видно, как достать значение второго уровня. В третьем уроке рядом встанут JSON и TOML: API viper один и тот же, меняется только расширение файла.
config.yaml
GOserver:
port: 8080
database:
host: localhost
port: 5432
timeouts:
read: 5sВложенные блоки задаются отступом: блок server с полем port в viper адресуется строкой server.port. Значение 8080 без кавычек парсится как число — GetString вернёт его строкой, GetInt числом. Файл лежит в той же директории, откуда запускается программа: относительный путь в SetConfigFile отсчитывается от рабочей директории, а не от места бинарника.
viper.SetConfigFile и viper.ReadInConfig: два вызова#
Первый вызов называет файл: viper.SetConfigFile("config.yaml") принимает точный путь, относительный или абсолютный. Расширение здесь несёт смысл: по нему viper выбирает парсер — yaml означает YAML, json — JSON, и переопределять ничего вручную не нужно. Второй вызов делает всю работу: viper.ReadInConfig() открывает файл, разбирает его и складывает содержимое в реестр, после чего ключи доступны через Get. Пока ReadInConfig не отработал, реестр пуст: ни один ключ из файла ещё не существует — и это главное отличие от SetDefault, который кладёт значения сразу. Вызовы выполняются один раз на старте; перечитывание конфига во время работы — отдельная механика WatchConfig, до неё дойдём в двенадцатом уроке. Существует и второй способ найти файл — SetConfigName("config") плюс AddConfigPath("."): viper сама обыщет каталоги и расширения. Пока держимся за SetConfigFile: явный путь убирает один слой магии, а поиск по локациям разберём отдельно.
main.go: программа, читающая yaml
GOpackage main
import (
"fmt"
"log"
"github.com/spf13/viper"
)
func main() {
viper.SetConfigFile("config.yaml")
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("ошибка чтения конфига: %v", err)
}
port := viper.GetInt("server.port")
host := viper.GetString("database.host")
fmt.Println("порт:", port)
fmt.Println("хост базы:", host)
}Вся программа — два вызова viper, обязательная проверка ошибки и два типизированных чтения. log.Fatalf гасит процесс с сообщением и ненулевым кодом выхода: без конфигурации боевой сервис честнее не поднимается вовсе. GetInt("server.port") вернёт 8080 как int, GetString("database.host") — localhost как строку. Запуск локальный: go mod init, go get github.com/spf13/viper, go run . — браузерная песочница сайта не имеет ни файловой системы, ни сторонних пакетов.
Ключи регистронезависимы, вложенность — через точку#
Две конвенции адресации в viper выучиваются сразу и на всю жизнь. Первая: ключи регистронезависимы. Viper приводит всё к нижнему регистру при загрузке, поэтому viper.GetString("database.host"), viper.GetString("DATABASE.HOST") и viper.GetString("Database.Host") вернут одно и то же. Это закрывает целый класс багов «в файле Host, в коде host», но имеет оборотную сторону: AllKeys() всегда отвечает строчными именами, а два ключа, различающиеся только регистром, в одном файле станут одним. Вторая конвенция: вложенность адресуется точкой. Блок server с полем port — ключ server.port; блок timeouts с полем read — timeouts.read. Точка в строке ключа — не символ, а разделитель пути по дереву конфига. Глубина не ограничена: четвёртый уровень YAML достаётся той же схемой. Развернуть дерево целиком в структуру — работа Unmarshal и mapstructure, об этом отдельный урок второй половины курса.
В файле port: 8080 записан числом, но viper.GetString("server.port") вернёт "8080", а viper.GetInt на строке "8080" вернёт число: приведением типов внутри viper занимается библиотека cast. Полезно и опасно одновременно: полезно, когда формат значения в файле менялся, опасно тем, что GetInt на значении "восемьдесят" молча вернёт 0 — без какой-либо ошибки. Семантику критичных значений проверяйте на старте.
Ошибки ReadInConfig: файл не найден и файл сломан#
Ветка if err != nil — не церемония, а половина урока: у ReadInConfig три типовых сценария неудачи, и у каждого свой смысл. Первый: файла нет. При поиске через SetConfigName и AddConfigPath viper вернёт viper.ConfigFileNotFoundError — специальный тип, который носит имя конфига и список просмотренных локаций: по нему видно, где программа искала файл. При явном SetConfigFile с несуществующим путём тип другой — обычная файловая ошибка вида open config.yaml: no such file or directory. Второй сценарий: файл есть, но парсинг сломался — таб вместо пробелов в отступе, пропущенный пробел после двоеточия, незакрытая кавычка. Viper завернёт ошибку парсера в viper.ConfigParseError, и текст придёт с номером строки: While parsing config: yaml: line 4: ... — сообщения достаточно, чтобы открыть файл и поправить его за минуту. Третий сценарий редкий, но коварный: расширение, которое viper не знает, даёт UnsupportedConfigError. Все три различаются типом, и реакции разводятся через errors.As.
Разные ошибки — разные реакции
GOif err := viper.ReadInConfig(); err != nil {
var notFound viper.ConfigFileNotFoundError
var parseErr viper.ConfigParseError
switch {
case errors.As(err, ¬Found):
log.Println("конфиг не найден, работаем на дефолтах")
case errors.As(err, &parseErr):
log.Fatalf("конфиг битый: %v", err)
default:
log.Fatalf("конфиг не прочитан: %v", err)
}
}errors.As распаковывает обёрнутые ошибки: ConfigParseError прячет внутри ошибку yaml-парсера с номером строки, и %v напечатает всю цепочку целиком. Отсутствие файла — штатная ситуация для утилиты с хорошими дефолтами; битый файл — всегда авария: молча продолжить нельзя, значений не будет. Порог реакции выбирается по продукту, но сама ветка ошибки не опциональна.
Уберите из программы обработку ошибки — и она продолжит работу с пустым реестром: GetInt("server.port") молча вернёт 0, GetString("database.host") — пустую строку. Сервис поднимется, начнёт слушать порт 0 или стучаться в пустой хост — и упадёт в проде в три ночи, а стек-трейс укажет на сетевую библиотеку, а не на конфиг. Viper не паникует на отсутствующих ключах — нулевые значения здесь норма языка. Единственная защита — обязательная ветка if err != nil после ReadInConfig.
Три ошибки ReadInConfig и реакция на них#
| Ошибка | Когда возникает | Разумная реакция |
|---|---|---|
| viper.ConfigFileNotFoundError | файл искали через SetConfigName и AddConfigPath и не нашли | работать на дефолтах или упасть — по продукту |
| файловая ошибка (no such file or directory) | SetConfigFile указал на несуществующий путь | падать с внятным сообщением о пути |
| viper.ConfigParseError | файл есть, синтаксис битый: таб, пропущенный пробел, кавычка | всегда падать: внутри ошибка парсера с номером строки |
| viper.UnsupportedConfigError | расширение файла viper не знает | падать или поправить расширение |
Ключа в файле нет, а программа его читает, — обычный сценарий: файл от другого окружения, ключ переименовали, опечатка в имени. Viper не паникует и не возвращает ошибку: GetString вернёт пустую строку, GetInt — 0, GetBool — false, Get — nil. Поведение последовательное, но диагностировать его неудобно: пустая строка неотличима от честно пустого значения из файла. Два инструмента помогают. viper.IsSet("server.port") отвечает на вопрос «задан ли ключ хоть в одном слое» — до чтения и без значения; его удобно вызывать на обязательных ключах и падать с перечнем недостающего. Дефолты через SetDefault делают вопрос бессмысленным для необязательных настроек: ключ всегда задан, и чтение возвращает осмысленное значение. Разбор SetDefault — пятый урок курса; здесь достаточно правила: то, без чего сервис не живёт, проверяйте IsSet или дефолтом, а остальное читайте с готовностью к нулю.
Чек-лист первого запуска#
- config.yaml лежит в рабочей директории, откуда запускается go run или бинарник: относительный путь в SetConfigFile отсчитывается от неё.
- Расширение файла соответствует формату: yaml для YAML, json для JSON — парсер выбирается по нему.
- Отступы в YAML — пробелы, не табуляция: таб ломает парсинг с ошибкой на строке.
- После ReadInConfig стоит обработка ошибки: без неё программа живёт с пустым реестром и нулями.
- Обязательные ключи проверены через viper.IsSet до использования.
- Для отладки напечатан viper.AllSettings(): видно всё, что реально попало в реестр.
Проверка: регистр ключей
В config.yaml записан блок Database с полем Host: db.internal. Что вернёт viper.GetString("database.host")?
Проверка: какой тип ошибки при чужом пути
Задан viper.SetConfigFile("confiq.yaml") — имя с опечаткой, файла нет. Что вернёт ReadInConfig?
Что напечатает программа с битым YAML?
Прочитайте код и выберите, что он выведет:
// config.yaml: отступ внутри блока server сделан табом,
// а не пробелами — YAML такой файл не примет
viper.SetConfigFile("config.yaml")
if err := viper.ReadInConfig(); err != nil {
fmt.Println("конфиг не прочитан:", err)
return
}
fmt.Println(viper.GetInt("server.port"))Соберите чтение конфига
Допишите два вызова viper и два типизированных чтения.
Соберите читателя YAML по строкам
Расставьте строки минимальной программы: от объявления пакета до печати значений.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: первый YAML
Диагностика и что дальше#
Программа напечатала пустую строку вместо хоста — разбор всегда один и тот же. Шаг первый: viper.AllSettings() в main сразу после чтения — перед вами всё содержимое реестра; ключа нет, значит файл не прочитан или ключ в файле назван иначе. Шаг второй: ошибка ReadInConfig — она была, а ветка забылась? Печать AllSettings и честная обработка ошибки закрывают девять случаев из десяти. Дальше по курсу — форматы: JSON, TOML и переключение между ними, затем типизированные Get и ловушка any, дефолты и окружение; полный маршрут всегда под рукой на странице курса viper. Если хочется сравнить с эпизодом из другой программы — в курсе Cobra есть урок про файлы конфигов, но здесь разбор идёт глубже. Все примеры урока требуют локального Go и файла на диске: у песочницы ни того, ни другого нет, поэтому проверяйте у себя.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Два вызова читают YAML: viper.SetConfigFile("config.yaml") называет файл, viper.ReadInConfig() разбирает его в реестр.
- Пока ReadInConfig не отработал, ключей из файла не существует: реестр пуст, Get отвечает нулями и пустыми строками.
- Ключи регистронезависимы: viper приводит всё к нижнему регистру, DATABASE.HOST и database.host — один ключ.
- Вложенность адресуется точкой: server.port, database.host, timeouts.read — путь по дереву конфига.
- ConfigFileNotFoundError приходит из режима поиска по локациям; явный SetConfigFile на несуществующий путь даёт файловую ошибку.
- Битый YAML приходит как ConfigParseError с номером строки внутри: реакция одна — падать с сообщением.
- Пропущенная обработка ошибки не роняет программу: GetString вернёт "", GetInt — 0, и прод упадёт позже и в другом месте.
Домашнее задание
Соберите программу урока локально: go mod init, go get github.com/spf13/viper, файл config.yaml с блоками server и database. Проверьте чтение порта и хоста, затем три поломки: переименуйте файл, сломайте отступ табом и удалите обработку ошибки. Запишите, что вернули GetString и GetInt в каждом случае, — конспект станет таблицей диагностики.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем SetConfigFile отличается от SetConfigName с AddConfigPath?
SetConfigFile принимает точный путь к конкретному файлу: viper открывает его и выбирает парсер по расширению. SetConfigName задаёт только имя без расширения, AddConfigPath добавляет каталоги для поиска — viper перебирает локации и форматы, а не найдя ничего, возвращает ConfigFileNotFoundError. Фиксированному пути сервисов подходит первый способ, утилитам — второй.
Можно ли читать YAML без viper, стандартной библиотекой?
В stdlib нет YAML-парсера — берут gopkg.in/yaml.v3 и размаршаливают файл в структуру вручную. Один источник это закрывает, но как только появляются окружение, флаги и дефолты с порядком перекрытия, объём ручного кода растёт на сотни строк. Viper полезна именно этим слоем: файл — один из её источников, а не единственный.
Почему viper не паникует на отсутствии ключа?
Нулевое значение — конвенция Go: GetString возвращает "", GetInt — 0, как и любое поле, которое никто не инициализировал. Библиотека не знает, обязателен ли ключ для вашего сервиса. Обязательность выражается на вашей стороне: SetDefault для необязательных настроек, viper.IsSet и падение на старте для обязательных — валидации посвящён отдельный урок курса.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context