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

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

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

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

Первый конфиг: читаем YAML через ReadInConfig

Чтение yaml в Go обычно начинается с viper: файл config.yaml лежит рядом с бинарником, а сервис всё ещё работает на числах, зашитых в код. Два вызова — SetConfigFile и ReadInConfig — решают основное, но между ними прячутся регистронезависимые ключи, вложенность через точку и три разных ошибки. Разберём первый конфиг построчно и посмотрим, что молча вернёт GetString, когда что-то не так.

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

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

Чтение yaml в Go: что лежит в config.yaml#

YAML — самый частый формат конфигов в мире Go: он же живёт в Kubernetes-манифестах, docker-compose и GitHub Actions, и писать конфиг сервиса в том же синтаксисе — решение без сюрпризов для ops. Формат читается как дерево: пары ключ: значение, вложенность задаётся отступом в два пробела, комментарии начинаются с решётки. Файл урока покрывает минимум сервиса — порт и хост базы — плюс вложенный блок таймаутов, чтобы к концу урока было видно, как достать значение второго уровня. В третьем уроке рядом встанут JSON и TOML: API viper один и тот же, меняется только расширение файла.

config.yaml

GO
server:
  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

GO
package 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, об этом отдельный урок второй половины курса.

Ошибки 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.

Разные ошибки — разные реакции

GO
if err := viper.ReadInConfig(); err != nil {
        var notFound viper.ConfigFileNotFoundError
        var parseErr viper.ConfigParseError

        switch {
        case errors.As(err, &notFound):
                log.Println("конфиг не найден, работаем на дефолтах")
        case errors.As(err, &parseErr):
                log.Fatalf("конфиг битый: %v", err)
        default:
                log.Fatalf("конфиг не прочитан: %v", err)
        }
}

errors.As распаковывает обёрнутые ошибки: ConfigParseError прячет внутри ошибку yaml-парсера с номером строки, и %v напечатает всю цепочку целиком. Отсутствие файла — штатная ситуация для утилиты с хорошими дефолтами; битый файл — всегда авария: молча продолжить нельзя, значений не будет. Порог реакции выбирается по продукту, но сама ветка ошибки не опциональна.

Три ошибки 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 и два типизированных чтения.

viper.("config.yaml") if err := viper.(); err != nil { log.Fatal(err) } port := viper.("server.port") host := viper.("database.host") fmt.Println(port, host)

Соберите читателя YAML по строкам

Расставьте строки минимальной программы: от объявления пакета до печати значений.

Строки

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

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

Карточки: первый YAML

Термин

viper.SetConfigFile

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

Называет конкретный файл конфигурации: точный путь, парсер выбирается по расширению. Пара к нему — ReadInConfig.

1 / 6

Диагностика и что дальше#

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

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

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

Чем 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 и падение на старте для обязательных — валидации посвящён отдельный урок курса.

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

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

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

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

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

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

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