---
title: "Конфиг-файлы: YAML, TOML и JSON"
description: "Viper yaml и не только: SetConfigName, AddConfigPath, SetConfigFile, обработка ConfigFileNotFoundError, примеры YAML, TOML и JSON и viper.Set из флагов."
course: "Cobra: CLI-приложения на Go"
url: "https://golangmaster.ru/courses/cobra/lessons/08-viper-fajly-konfigov/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

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

*Курс «Cobra: CLI-приложения на Go», урок 8 из 25. ~20 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/cobra/lessons/08-viper-fajly-konfigov/*

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

Каждый пользователь вашей утилиты рано или поздно произносит одну и ту же фразу: порт у меня всегда один и тот же, дайте написать его в файл. Это запрос на конфиг-файл — третий источник настроек после дефолтов и флагов. Задача звучит просто, а вопросов в ней на вечер: где лежит файл — в текущем каталоге, в домашнем, в 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, но не падайте. Валидация после чтения стоит сорок строк и спасает от класса тикетов «а почему у меня не работает», который живёт месяцами.

> **SetConfigName с расширением.** viper.SetConfigName("notes.yaml") — фаворит среди ошибок урока. Имя ищется как есть: Viper переберёт расширения и будет искать notes.yaml.yaml, notes.yaml.toml — и не найдёт ничего, при любом содержимом диска. В SetConfigName кладут только имя; формат задаёт SetConfigType или реальное расширение файла.

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

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

- nil: файл читается штатно
- ✅ ConfigFileNotFoundError: имя уже с расширением
- ConfigParseError: YAML не разобрался
- Панику: так нельзя вызывать поиск

SetConfigName принимает имя без расширения: Viper ищет notes.yaml.yaml и другие комбинации и не находит ничего. Ошибка — ConfigFileNotFoundError, штатный сигнал «файла нет», а не парсинга.

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

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

- Напечатать предупреждение и работать на дефолтах
- ✅ Упасть с os.Exit(1) и понятным сообщением
- Молча игнорировать файл: пользователь сам разберётся
- Перезаписать файл дефолтами

Найденный, но нечитаемый конфиг — ошибка, а не отсутствие: пользователь думает, что его настройки работают, а утилита живёт на дефолтах. ConfigParseError и родня требуют падения с сообщением; падение уместно только здесь, а ConfigFileNotFoundError — рабочий случай.

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

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

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

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

- ✅ «8080» и 8080: GetString превратил число в строку
- Пустую строку и 0: порт лежит в другом ключе
- 8080 и панику: число нельзя печатать строкой
- «8080» и ошибку чтения

После ReadInConfig ключ server.port содержит число 8080. GetString приводит его к строке через cast, GetInt читает как есть — приведение из седьмого урока работает в обе стороны и молчит.

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

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

```go
viper.SetConfig___("notes")
viper.AddConfig___("$HOME/.notes")
viper.SetConfig___("yaml")

if ___ := viper.ReadInConfig(); err != nil {
        fmt.Fprintln(os.Stderr, err)
}
```

Подсказка: Имя файла задаёт SetConfigName, каталог — AddConfigPath, формат — SetConfigType; ошибка чтения принимается в err.

Правильные ответы в пропусках: `Name`, `Path`, `Type`, `err`

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

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

Правильный порядок строк:

1. `viper.SetConfigName("notes")`
2. `viper.AddConfigPath("$HOME/.notes")`
3. `viper.SetConfigType("yaml")`
4. `err := viper.ReadInConfig()`
5. `if err == nil {`
6. `  fmt.Println("читаю конфиг:", viper.ConfigFileUsed())`

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

- **viper.SetConfigName** — Имя файла без расширения: Viper переберёт поддерживаемые форматы. «notes.yaml» — ошибка: искать будет notes.yaml.yaml.
- **viper.AddConfigPath** — Добавляет каталог поиска; вызывается многократно, порядок = приоритет. $HOME и другие переменные раскрываются.
- **viper.SetConfigFile** — Явный путь вместо поиска — обычно из флага --config. Для файла без расширения обязателен SetConfigType.
- **ConfigFileNotFoundError** — Файла нет ни в одном каталоге. Штатная ситуация первого запуска: работаем на дефолтах, не падаем.
- **viper.Set** — Самый сильный слой реестра: перекрывает файл, окружение и флаги. Классика — перенос явно переданных флагов после разбора.

### Что дальше

Файл читается, но переменные окружения пока бессильны — следующий урок, [переменные окружения: AutomaticEnv](/courses/cobra/lessons/09-peremennye-okruzheniya/), добавит четвёртый слой для CI и docker-compose. Дальше — [расстановка приоритетов](/courses/cobra/lessons/10-prioritety-konfiguraciya/) с BindPFlag и Unmarshal, где все источники соберутся в одну структуру. Для параноиков пригодится урок про [watch и секреты](/courses/cobra/lessons/24-viper-watch-sekrety/): горячая перезагрузка конфига и что нельзя класть в YAML. Обзор курса — на странице [Cobra](/courses/cobra/). И стандартная оговорка: чтение файла проверяется только локально — соберите утилиту, сломайте YAML и прочитайте сообщение об ошибке; песочница сайта файловую систему не исполняет.

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

- Поиск конфига: 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, где пропущен отступ, — и убедитесь, что поведение во всех трёх случаях разное и осмысленное.

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

### Как заставить 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 берут для сгенерированных файлов и интеграций: без комментариев, зато парсится чем угодно.
