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

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

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

Урок 8 из 25 ~20 мин +50 XP

Конфиг-файлы: YAML, TOML и JSON

Для Viper YAML — родной формат конфиг-файлов, но одной строкой меняется на TOML или JSON. В уроке — поиск файла через SetConfigName и AddConfigPath, явный путь через SetConfigFile, обработка ConfigFileNotFoundError на первом запуске и перенос флагов в реестр через viper.Set.

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

Каждый пользователь вашей утилиты рано или поздно произносит одну и ту же фразу: порт у меня всегда один и тот же, дайте написать его в файл. Это запрос на конфиг-файл — третий источник настроек после дефолтов и флагов. Задача звучит просто, а вопросов в ней на вечер: где лежит файл — в текущем каталоге, в домашнем, в XDG-путях? Что делать, когда файла нет — падать или работать на дефолтах? Что делать, когда файл есть, но битый? И какой формат выбрать, чтобы пользователь не проклял и файл, и вас. Viper закрывает все четыре вопроса: три вызова настраивают поиск, ошибка чтения различает «нет файла» и «битый файл», а формат определяется по расширению — YAML, TOML, JSON и ещё полдюжины.

Конфиг-файл в Viper: три вызова поиска#

Каноническая тройка выглядит так: SetConfigName задаёт имя файла без расширения, AddConfigPath добавляет каталог для поиска, SetConfigType называет формат. Viper ищет notes.yaml, notes.toml, notes.json — перебирает все поддерживаемые расширения — в каждом добавленном каталоге. Каталоги проверяются в порядке добавления: первый найденный файл побеждает, поэтому рабочий каталог добавляют раньше домашнего — локальный конфиг перекрывает глобальный. Пути с $HOME Viper раскрывает сама через absPathify. SetConfigType нужен как запасной формат, когда расширения нет совсем — например, файл подают на stdin; при обычном поиске по имени он не обязателен, формат подскажет расширение на диске. Всё это живёт в функции initConfig, которую регистрируют через cobra.OnInitialize: она выполнится перед Run любой команды — и перед serve, и перед list, — что и требуется для настроек уровня приложения.

initConfig: поиск файла, чтение, обе ошибки

GO
package cmd

import (
        "fmt"
        "os"

        "github.com/spf13/cobra"
        "github.com/spf13/viper"
)

func initConfig() {
        viper.SetConfigName("notes")
        viper.AddConfigPath(".")
        viper.AddConfigPath("$HOME/.notes")
        viper.SetConfigType("yaml")

        if err := viper.ReadInConfig(); err != nil {
                if _, ok := err.(viper.ConfigFileNotFoundError); ok {
                        // Файла нет — это норма на первом запуске
                        fmt.Fprintln(os.Stderr, "конфиг не найден, работаю с дефолтами")
                } else {
                        // Файл есть, но не читается — падаем сразу
                        fmt.Fprintln(os.Stderr, "ошибка конфига:", err)
                        os.Exit(1)
                }
        } else {
                fmt.Println("читаю конфиг:", viper.ConfigFileUsed())
        }
}

func init() {
        cobra.OnInitialize(initConfig)
}

Три вызова поиска складываются до чтения; ReadInConfig возвращает ошибку двух пород. ConfigFileNotFoundError означает, что файла нигде не нашлось — это штатная ситуация первого запуска, работаем дальше на дефолтах. Любая другая ошибка — файл найден, но не читается: битый YAML, нет прав; молчать нельзя, утилита обязана упасть с понятным сообщением. ConfigFileUsed печатает, какой файл победил в поиске.

Где хранить конфиг: домашний каталог и XDG#

Выбор каталогов для AddConfigPath — половина пользовательского опыта. Классический минимум — два места: текущий каталог, где лежат проекты, и домашний каталог утилиты. На Linux приличные утилиты уважают XDG: переменная XDG_CONFIG_HOME или ~/.config по умолчанию, то есть ~/.config/notes/notes.yaml; на macOS ждут ~/Library/Application Support, на Windows — %APPDATA%. Вручную это выглядит как switch по runtime.GOOS, и на старте проекта обычно пишут ~/notes, а дозревают до XDG-библиотек вроде adrg/xdg. Практическое правило: серверной утилите без «проектов» рабочий каталог в списке не нужен — конфиг от текущей директории делает поведение зависимым от того, откуда утилиту позвали. Для локальных инструментов наоборот: каталог проекта первым — так у каждого репозитория свои настройки.

Порядок каталогов: кто кого перекрывает#

  • Каталоги проверяются в порядке добавления: первый найденный файл побеждает
  • Рабочий каталог «.» добавляют первым для локальных инструментов: конфиг проекта перекрывает глобальный
  • Домашний каталог идёт вторым: он ловит настройки по умолчанию для всех проектов
  • Для серверных утилит рабочий каталог не добавляют: поведение не должно зависеть от cwd
  • XDG-пути вроде ~/.config/notes — компромисс между приличием и простотой: одна строка вместо switch по ОС

ReadInConfig и две породы ошибок#

ReadInConfig — единственная вызываемая вручную часть схемы, и обработка её ошибки — главный навык урока. Первая порода — viper.ConfigFileNotFoundError: файла нет ни в одном каталоге поиска. Падать на ней нельзя: первый запуск утилиты всегда проходит без конфига, и «notes: конфиг не найден, работаю с дефолтами» в stderr — нормальное поведение, а не сбой. Вторая порода — всё остальное, чаще всего viper.ConfigParseError: файл нашёлся, но парсер его не осилил. Здесь молчание опасно: пользователь думает, что его настройки работают, а утилита тихо живёт на дефолтах. Падаем с os.Exit(1) и текстом ошибки — viper вкладывает в неё имя файла и причину. Различение делается type assertion на viper.ConfigFileNotFoundError; если вы уже привыкли к errors.As из продвинутого курса, здесь сработает и он. Проверить обе ветки вживую можно только локально: соберите утилиту, запустите без файла, потом намеренно сломайте YAML — удалите двоеточие и сравните сообщения.

notes.yaml: конфиг менеджера заметок

GO
# notes.yaml — конфиг менеджера заметок
server:
  host: 127.0.0.1
  port: 8080
  timeouts:
    read: 5s
    write: 10s

storage:
  path: ~/.notes
  max_backups: 3

verbose: false
tags:
  - go
  - cli

Вложенность делается отступами, комментарии начинаются с решётки — формат дружелюбен к ручному редактированию. Ключи превращаются в те самые dotted-ключи из прошлого урока: viper.GetInt("server.port") вернёт 8080, viper.GetDuration("server.timeouts.read") — пять секунд. Список tags читается GetStringSlice.

YAML, TOML и JSON: что выбрать для конфиг-файла#

Viper читает форматы одинаково, а вот пользователь — нет, поэтому выбор формата — вопрос эмпатии. YAML человечнее всех: комментарии, минимальный шум, глубокая вложенность читается колонками. Его слабые места — отступы, из-за которых файл легко сломать, и нетипизированные скаляры: автор конфига легко получит строку там, где ждал число. TOML — выбор перфекционистов: секции в квадратных скобках, типы значений объявлены явно строкой в кавычках или числом без неё, отступы не имеют силы. На глубокой вложенности он становится многословным — секция в секции в секции. JSON — без комментариев и с обязательными запятыми, но зато знаком каждому инструменту: если конфиг генерируется другой программой, JSON самый предсказуемый. Для утилит, редактируемых человеком, я беру YAML и не помню сожалений; для сгенерированных файлов — JSON.

notes.toml: тот же конфиг в TOML

GO
# notes.toml — тот же конфиг в TOML
[server]
host = "127.0.0.1"
port = 8080

[server.timeouts]
read = "5s"
write = "10s"

[storage]
path = "~/.notes"
max_backups = 3

verbose = false
tags = ["go", "cli"]

Секции в скобках заменяют отступы, строковые значения объявлены кавычками, числа — голые. Ключи после чтения те же: server.port, server.timeouts.read. Чтобы Viper начала читать TOML, достаточно положить файл рядом с именем notes.toml — расширение подскажет формат, код initConfig не меняется.

Сравнение форматов конфигов#

ФорматКомментарииСильная сторонаСлабая сторона
YAMLда, с решёткичитаемая вложенность, минимум шумаотступы ломаются, скаляры нетипизированы
TOMLда, с решёткиявные типы, отступы не важныглубокая вложенность многословна
JSONнетпарсится везде, родной для генераторовбез комментариев, запятые и скобки

Секреты в конфиге: что там не должно оказаться#

Конфиг-файл — самое соблазнительное место для токена: удобно, работает, никто не видит. Не кладите туда секреты в открытом виде: файл попадает в бэкапы, git-историю, скриншоты и утилиты синхронизации, а YAML с токеном, случайно попавший в репозиторий, живёт в истории вечно. Правильная раскладка: в файле — всё, что не секрет, то есть порты, пути, таймауты; секреты приходят переменными окружения или флагом при запуске — ровно та иерархия, которую мы собираем в курсе. Специальные случаи — env-подстановка в YAML и шифрованные хранилища — разбираются в уроке про watch и секреты; там же — как не напечатать секрет в AllSettings при отладке. Мой практический тест: представьте конфиг на слайдах конференции. Если страшно — файл собран неправильно.

SetConfigFile и флаг --config: явный путь вместо поиска#

Иногда поиск не нужен: пользователь сам говорит, какой файл читать, — флагом --config. Тогда вместо тройки вызовов ставится один viper.SetConfigFile("/path/to/notes.yaml"): путь явный, расширение подсказывает формат, и поиск по каталогам не выполняется. Единственная ловушка — файл без расширения: тогда SetConfigType обязан назвать формат, иначе ReadInConfig не поймёт, чем парсить. Флаг --config объявляют персистентным на корне, чтобы он работал из любой подкоманды, а разбор делают в initConfig: если флаг передан — SetConfigFile, если нет — обычный поиск. Это тот самый каркас, который генерирует cobra-cli для связки Cobra и Viper. Нюанс от меня: не подставляйте в SetConfigFile дефолтный путь «на всякий случай» — этим вы выключите поиск и удивите пользователей, у которых конфиг лежит в домашнем каталоге.

viper.Set: перенос флагов в реестр#

Последний кусок пазла — флаги, которые пока живут вне реестра Viper. Перенос делается методом viper.Set: он кладёт значение в самый сильный слой реестра, сильнее файла и окружения. Классический приём — переносить только те флаги, что пользователь передал явно: чтение по Changed из пятого урока, запись через viper.Set по тому же ключу, что в конфиге. Получается честная иерархия без магии: конфиг задал порт, флаг его перекрыл, дефолт ждал в нижнем слое. Код переноса живёт в Run или в хуке PersistentPreRun — там, где флаги уже разобраны; про хуки и их порядок будет тринадцатый урок. Альтернатива ручному переносу — viper.BindPFlag, штатная привязка флагов к реестру, но у неё есть нюансы с порядком инициализации, поэтому ей посвящён отдельный урок о приоритетах.

Флаг перекрывает конфиг: перенос через viper.Set

GO
// Внутри Run или PersistentPreRun, где флаги уже разобраны:
if cmd.Flags().Changed("port") {
        port, _ := cmd.Flags().GetInt("port")
        viper.Set("server.port", port)
}

// Дальше весь код читает одно место:
fmt.Println(viper.GetInt("server.port"))

Переносим значение только при Changed — иначе флаг затёр бы конфиг дефолтом, и иерархия сломалась. viper.Set кладёт значение в самый сильный слой: теперь и конфиг, и флаг видны в одном ключе, а бизнес-код читает один реестр вместо двух источников.

IsSet и валидация после чтения#

После ReadInConfig реестр наполнен, но это не значит, что он корректен. Файл мог содержать опечатку в ключе — viper.GetInt("server.porr") вернёт 0 без намёка на проблему, — мог задать конфликтующие значения, мог вообще оказаться чужим конфигом от другой утилиты с тем же именем. Слой проверки пишется такой же, как Validate из шестого урока: IsSet для обязательных ключей, диапазоны для чисел, связность для пар вроде «watch требует путь к хранилищу». Полезный приём — проверка неизвестных ключей: AllKeys даёт список всего, что видит Viper, и если в нём есть ключ, которого нет в вашем списке настроек, — скорее всего, опечатка в конфиге; предупредите пользователя в stderr, но не падайте. Валидация после чтения стоит сорок строк и спасает от класса тикетов «а почему у меня не работает», который живёт месяцами.

Проверка: имя и расширение

На диске лежит ~/.notes/notes.yaml с верным содержимым. Код вызвал viper.SetConfigName("notes.yaml") и AddConfigPath("$HOME/.notes"). Что вернёт ReadInConfig?

Проверка: битый конфиг

Файл notes.yaml найден, но в нём пропущен отступ, и YAML не парсится. Как правильно поступить?

Что напечатает GetString после чтения YAML

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

// notes.yaml прочитан успешно:
//   server:
//     port: 8080

fmt.Println(viper.GetString("server.port"))
fmt.Println(viper.GetInt("server.port"))

Восстановите настройку поиска конфига

Впишите окончания методов: имя, путь, тип и переменную ошибки.

viper.SetConfig("notes") viper.AddConfig("$HOME/.notes") viper.SetConfig("yaml") if := viper.ReadInConfig(); err != nil { fmt.Fprintln(os.Stderr, err) }

Соберите настройку чтения конфига из шести строк

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

Строки

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

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

Карточки: конфиг-файлы в Viper

Термин

viper.SetConfigName

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

Имя файла без расширения: Viper переберёт поддерживаемые форматы. «notes.yaml» — ошибка: искать будет notes.yaml.yaml.

1 / 5

Что дальше#

Файл читается, но переменные окружения пока бессильны — следующий урок, переменные окружения: AutomaticEnv, добавит четвёртый слой для CI и docker-compose. Дальше — расстановка приоритетов с BindPFlag и Unmarshal, где все источники соберутся в одну структуру. Для параноиков пригодится урок про watch и секреты: горячая перезагрузка конфига и что нельзя класть в YAML. Обзор курса — на странице Cobra. И стандартная оговорка: чтение файла проверяется только локально — соберите утилиту, сломайте YAML и прочитайте сообщение об ошибке; песочница сайта файловую систему не исполняет.

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

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

Вывод

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

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

  • Поиск конфига: SetConfigName без расширения, AddConfigPath в порядке приоритета, SetConfigType как запасной формат.
  • ReadInConfig даёт две породы ошибок: ConfigFileNotFoundError — файла нет, это норма; остальное — файл есть, но не читается.
  • Найденный, но битый конфиг — падение с os.Exit(1); отсутствие файла — работа на дефолтах.
  • Viper читает YAML, TOML, JSON и другие форматы, определяя их по расширению.
  • SetConfigFile отключает поиск: явный путь из флага --config, для файла без расширения нужен SetConfigType.
  • viper.Set — самый сильный слой: переносите в него флаги только при Changed.
  • SetConfigName с расширением — классическая ошибка: файл не найдётся никогда.

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

Соберите initConfig из урока в своей утилите: поиск notes.yaml в рабочем каталоге и ~/.notes, обработку обеих ошибок и печать ConfigFileUsed. Затем проведите три запуска: без файла, с корректным YAML и с YAML, где пропущен отступ, — и убедитесь, что поведение во всех трёх случаях разное и осмысленное.

Личные заметкиПусто
Хранятся только в вашем браузере (Cobra · Конфиг-файлы: YAML, TOML и JSON)

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

Как заставить Viper читать конфиг-файл YAML?

viper.SetConfigName("notes"), viper.AddConfigPath("."), viper.ReadInConfig() — расширение .yaml на диске подскажет формат. Для файла без расширения добавьте viper.SetConfigType("yaml"), а для явного пути — viper.SetConfigFile.

Почему ReadInConfig возвращает ConfigFileNotFoundError?

Либо файла действительно нет ни в одном каталоге поиска, либо в SetConfigName передали имя с расширением — тогда Viper ищет notes.yaml.yaml и не находит ничего. Имя задаётся без расширения: формат определяет файл на диске или SetConfigType.

Какой формат конфига выбрать: YAML, TOML или JSON?

Для файлов, которые редактирует человек, — YAML: комментарии и читаемая вложенность. Для строгих настроек с явными типами — TOML. JSON берут для сгенерированных файлов и интеграций: без комментариев, зато парсится чем угодно.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

Bubble Tea25 уроков~10 чпрактика25 уроков уже доступны

Bubble Tea: TUI-приложения в терминале

Интерактивный курс Bubble Tea — TUI-фреймворка на Go: Elm-архитектура, bubbles, Lip Gloss, мышь и клавиатура. 25 уроков с примерами и итоговым приложением.

  • Понимать Elm-архитектуру: Model, Update, View, Init
  • Обрабатывать клавиатуру и мышь в терминале
  • Писать асинхронные операции через tea.Cmd, Batch и Sequence
  • Использовать bubbles: list, textinput, textarea, table, viewport, progress, spinner