Содержание урока · 11 разделов
- viper WriteConfig: куда пишет метод
- Что попадёт в файл: AllSettings и дефолты
- SafeWriteConfig и SafeWriteConfigAs: не трогать существующее
- WriteConfigTo: шаблон конфига в io.Writer
- Метод, цель и перезапись
- Когда WriteConfig опасен: git, секреты, чужие руки
- Гигиена записи конфига
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Программа прочитала config.yaml, поверх него вызвала пару Set, заработали дефолты — и всё это живёт только в памяти процесса: перезапуск, и состояние снова собирать с нуля. viper WriteConfig закрывает обратную задачу: он выгружает текущий реестр — то, что viper видит через AllSettings, — в файл на диск. Звучит как мелочь рядом с чтением, но именно запись рождает два сюжета: команду init, которая генерирует стартовый конфиг, и сценарий «сохрани мои правки», который регулярно ломает прод. Разберём пять методов записи — WriteConfig, WriteConfigAs, SafeWriteConfig, SafeWriteConfigAs и WriteConfigTo, — что именно попадёт в файл, зачем нужен SetConfigType при записи в новый файл и почему комментарии в записанном конфиге не выживают. Код урока пишет на диск, поэтому запускается локальным Go: браузерная песочница файловым вводом-выводом не занимается.
viper WriteConfig: куда пишет метод#
У WriteConfig нет аргумента-пути: место записи вычисляется из того, что уже известно viper. Если файл конфига был найден через SetConfigName плюс AddConfigPath или задан SetConfigFile, WriteConfig пишет именно туда — по тому же пути, который сработал для ReadInConfig. Целевой файл не определён — метод вернёт ошибку: viper не угадывает, куда сохранять, и каталог сама не выбирает. Второе условие — известный формат: тип определяется расширением файла или SetConfigType, и без него маршалинг не стартует. Пишется всё, что viper отдаёт через AllSettings, — об этом следующий раздел. И главное свойство: перезапись молчаливая. Существующий файл заменяется целиком, без вопросов, слияния ключей и бэкапов. Для «сохрани состояние» это удобно, а там, где файл писали руками, — опасно: к этой стороне записи вернёмся отдельно.
WriteConfig: сохранить правки в тот же файл
GO// configs/config.yaml до запуска: server: {host: "0.0.0.0", port: 8080}
viper.SetConfigFile("configs/config.yaml")
if err := viper.ReadInConfig(); err != nil {
log.Fatal(err)
}
// флаг --port осел в реестре через Set
viper.Set("server.port", flagPort)
// сохраняем состояние: файл получит и файловые значения, и Set
if err := viper.WriteConfig(); err != nil {
log.Fatalf("запись конфига: %v", err)
}WriteConfig нашёл файл через SetConfigFile и перезаписал его дампом реестра: после вызова в configs/config.yaml лежит server.port от флага, а не исходное значение. Ошибка проверяется обязательно: файл не найден или тип неизвестен — записи не было. Обратной дороги нет: старый порт из файла затёрт, и восстановления viper не хранит.
Что попадёт в файл: AllSettings и дефолты#
WriteConfig сериализует реестр целиком, и это стоит проговорить предметно. В дамп попадают: значения из прочитанного файла; всё, что задано Set — программа часто даже не догадывается, что её правка запечётся в файл; дефолты из SetDefault — включая те, что никогда не перекрывались; ключи с явной привязкой BindEnv и BindPFlag — со значениями на момент записи. Не попадают ключи AutomaticEnv: viper не умеет перечислять переменные окружения процесса, список взять неоткуда, — тот же предел, что у AllKeys. Структура восстанавливается из плоских ключей реестра: server.port снова станет вложенной секцией. А вот исходный порядок ключей и оформление файла сохранять не обязаны: реестр — это карта map[string]any, и маршалер пишет её так, как решит сам. Для дефолтов это отдельный плюс: конфиг с запечёнными значениями по умолчанию — готовый шаблон, о чём ниже.
Реальная история: CLI-утилита собирала боевой конфиг командой init, и первым вопросом стало «как добавить в сгенерированный файл комментарии-подсказки?» Ответ: никак. viper хранит значения, а не текст файла — комментарии, порядок ключей и форматирование при записи выбрасываются. Хочется шаблон с подсказками — клади рядом yaml-заготовку и копируй её, а viper используй только для подстановки значений.
SafeWriteConfig и SafeWriteConfigAs: не трогать существующее#
Молчаливая перезапись — главное свойство WriteConfig и главная причина ожогов. Пара Safe-методов переворачивает правило: SafeWriteConfig пишет в файл, который задаётся путями поиска и типом конфига, но только если его ещё нет; SafeWriteConfigAs — в указанный путь на тех же условиях. Существующий файл — это ошибка ErrConfigFileAlreadyExists и гарантия, что содержимое не тронуто: ни дозаписи, ни слияния ключей. Именно Safe-варианты нужны команде init: при первом запуске генерируем конфиг, при повторном — честно сообщаем, что файл уже существует, вместо того чтобы молча затирать настройки пользователя. Разница внутри четвёрки только в месте записи: WriteConfig и SafeWriteConfig пишут туда, куда указывает конфигурация поиска, а As-варианты берут путь из аргумента — WriteConfigAs("out.yaml") создаст файл ровно там, где сказано, и формат возьмёт из расширения.
Команда init: только в несуществующий файл
GOfunc runInit() error {
viper.SetDefault("server.host", "0.0.0.0")
viper.SetDefault("server.port", 8080)
viper.SetDefault("worker.timeout", "30s")
if err := viper.SafeWriteConfigAs("config.yaml"); err != nil {
return fmt.Errorf("config.yaml уже существует: %w", err)
}
fmt.Println("создан config.yaml с настройками по умолчанию")
return nil
}Реестр перед записью наполнен одними дефолтами — этого достаточно: ReadInConfig перед записью не обязателен. Повторный запуск вернёт ошибку о существующем файле, и настройки пользователя останутся целыми. Расширение .yaml само говорит маршалеру формат — SetConfigType здесь не нужен.
WriteConfigTo: шаблон конфига в io.Writer#
Четвёртая цель записи — вообще не файл: WriteConfigTo принимает io.Writer и сливает туда дамп реестра. bytes.Buffer — для генерации строки; os.Stdout — для отладочной печати; файл, открытый вручную через os.Create, — когда нужен полный контроль над путём и правами. Типовой сценарий — команда init или экспорт настроек: yaml-дамп уезжает в буфер, дальше вы печатаете его в терминал, вставляете в шаблон или отдаёте по сети. Ас- и Safe-варианты тут не при чём: писатель выбрали вы сами, перезапись — ваша зона ответственности. Тип формата при WriteConfigTo берётся из SetConfigType: расширения у потока нет, угадывать не из чего. Это единственный из пяти методов, который сам не трогает диск, — а значит, безопасен по определению и отлично годится для тестов: сравнили содержимое буфера с ожидаемым yaml, и проверка записи готова.
Экспорт настроек в буфер
GOviper.SetDefault("server.host", "0.0.0.0")
viper.SetDefault("server.port", 8080)
viper.SetConfigType("yaml")
buf := new(bytes.Buffer)
if err := viper.WriteConfigTo(buf); err != nil {
log.Fatal(err)
}
fmt.Println(buf.String())
// server:
// host: 0.0.0.0
// port: 8080Ни чтения файла, ни пути на диске: реестр собран из дефолтов, SetConfigType объявил формат, WriteConfigTo отдал yaml в буфер. Так строится экспорт настроек и заготовка для команды init в CLI на Cobra: строку из буфера можно напечатать, сравнить в тесте или дополнить комментариями вручную.
Метод, цель и перезапись#
| Метод | Куда пишет | Перезаписывает ли |
|---|---|---|
| WriteConfig | файл из SetConfigFile или найденный поиском | да: молча и целиком |
| WriteConfigAs("out.yaml") | путь из аргумента | да: молча |
| SafeWriteConfig | файл, собранный из путей поиска и типа | нет: существующий даёт ошибку |
| SafeWriteConfigAs("out.yaml") | путь из аргумента | нет: существующий даёт ошибку |
| WriteConfigTo(w) | io.Writer: буфер, stdout, открытый файл | файл сам не трогает |
Когда WriteConfig опасен: git, секреты, чужие руки#
Моё рабочее правило, спорное ровно наполовину: WriteConfig запрещён в долгоживущих сервисах и разрешён только в CLI-командах. История, после которой правило появилось: сервис с записью на старте жил рядом с config.yaml в репозитории, и файл был документацией — с комментариями, группировкой, годами выверенным порядком ключей. Один запуск при изменившемся окружении — и в git прилетает дифф на сотни строк: комментарии исчезли, порядок перемешан, правки коллег затёрты реестром. Ревью ослепло, blame обесценился. Вторая опасность — секреты: значения из окружения уехали в файл рядом с кодом, и хранилище теперь расследует утечку. Третья — форматирование чужих рук: viper пишет карту, а не редактирует текст, так что любая красота в файле расходная. Граница применимости честная: конфиг как артефакт машины, который программа обслуживает сама, — запись оправдана; конфиг как документ в репозитории — нет.
Файл, созданный viper, получает обычные права создания — на многопользовательской машине читать его смогут все. Конфиг с паролями в таком файле — дыра: после записи сузьте права через os.Chmod до 0600 или вообще не пишите секреты на диск — для них в курсе отведён отдельный урок, а файл должен оставаться местом нечувствительных настроек.
Гигиена записи конфига#
- Запись — инструмент CLI-команд init и export; в демонах WriteConfig выключен: конфиг в репозитории переживёт ревью.
- Не уверены, что файл новый, — берите Safe-варианты: ошибка вместо молчаливой перезаписи дешевле затёртых настроек.
- Пишете в новый файл без расширения — объявите формат через SetConfigType, иначе маршалинг вернёт ошибку.
- Перед записью напечатайте дамп через WriteConfigTo в буфер: то, что увидите, — ровно то, что окажется на диске.
- Комментарии и порядок ключей не сохраняются: шаблон с подсказками держите отдельной заготовкой, а не надейтесь на viper.
- После записи сужайте права файла: 0600 для всего, что может содержать чувствительные значения.
Проверка: WriteConfig вернул ошибку
SetConfigName, AddConfigPath и SetConfigType вызваны, ReadInConfig не выполнялся, файла на диске нет. viper.WriteConfig() вернула ошибку. Почему?
Проверка: SafeWriteConfig на живом файле
config.yaml существует и содержит живые настройки пользователя. Что сделает SafeWriteConfig() при заданных путях поиска?
Что окажется в файле?
Прочитайте код и выберите, что он выведет:
// configs/config.yaml до запуска: server: {host: "old.local"}
viper.SetConfigFile("configs/config.yaml")
_ = viper.ReadInConfig()
viper.Set("server.host", "new.local")
_ = viper.WriteConfig()Соберите три сценария записи
Допишите методы: дамп в буфер, защищённая запись по пути и запись в найденный файл конфига.
Соберите команду init по строкам
Расставьте строки: два дефолта, один Set, защищённая запись в новый файл и подтверждение в выводе.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: запись конфига
Что дальше#
Запись на диск закрыта, и следующий урок — Sub: поддеревья конфига: общий реестр разрезается на изолированные срезы для модулей. Полный маршрут по темам лежит на странице курса viper. Домашний опыт — локальным Go: соберите команду init из урока, добейтесь, чтобы второй запуск не трогал существующий config.yaml, а дамп WriteConfigTo в тесте совпал с ожидаемым yaml. Заодно проверьте собственный код: нет ли в нём WriteConfig, который молча перезаписывает файл, собранный руками.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- WriteConfig выгружает реестр целиком — значения файла, Set-ключи, дефолты — в найденный или заданный файл конфига, молча перезаписывая его.
- Цель записи должна быть известна: SetConfigFile или пути поиска; без них метод вернёт ошибку, а не угадает место.
- SafeWriteConfig и SafeWriteConfigAs пишут только в несуществующий файл: существующий даёт ErrConfigFileAlreadyExists — основа команды init.
- WriteConfigTo сливает дамп в io.Writer — буфер, stdout, открытый файл; сам диск не трогает и годится для экспорта и тестов.
- В дамп попадают дефолты и явно привязанные источники; AutomaticEnv не перечисляется и в файл не пишется.
- Комментарии, порядок ключей и форматирование не выживают: viper маршалит карту значений, а не редактирует текст.
- Правило против ожогов: WriteConfig — только в CLI-командах init и export; в сервисах, чьи конфиги живут в git, запись выключена.
Домашнее задание
Соберите команду init: три SetDefault, SafeWriteConfigAs("config.yaml") с обработкой ошибки существующего файла. Прочитайте созданный файл локальным Go через ReadInConfig и убедитесь, что дефолты доехали до GetString. Затем добавьте дамп WriteConfigTo в буфер и напечатайте его, не трогая диск, — сравните с содержимым файла.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему в записанном файле другой порядок ключей и нет комментариев?
Потому что viper пишет не текст, а карту значений: реестр — это map[string]any, и маршалер сериализует её по своим правилам. Комментарии, группировка и исходный порядок в реестре не живут. Шаблон с подсказками держите отдельной заготовкой и подставляйте в неё значения.
Попадут ли значения из переменных окружения в записанный файл?
Зависит от привязки. Явные BindEnv-ключи попадут — со значениями на момент записи. А AutomaticEnv нет: viper не умеет перечислять переменные процесса, список взять неоткуда. Отсюда практическое следствие: секреты из окружения в дамп не утекут, но и настройки из env в файле не появятся.
Зачем SafeWriteConfig, если можно проверить os.Stat самому?
Проверку вручную легко забыть или написать с гонкой между проверкой и созданием. Safe-варианты выражают намерение типом метода и возвращают готовую ошибку ErrConfigFileAlreadyExists. Для команды init этого достаточно: первый запуск создаёт файл, повторный — возвращает ошибку, ничего не портя.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Sub(): разбираем конфиг на поддеревья
- RegisterAlias: синонимы ключей и миграция схемы
- Профили dev, staging и prod
- Секреты: почему пароли не живут в YAML
- WatchConfig и OnConfigChange: живой конфиг
- Вложенные ключи: деревья и mapstructure
- Unmarshal: конфиг целиком в структуру
- Приоритеты источников: кто кого перекрывает
- Все курсы по Go
Следующая ступень учебного пути
Chi: роутер для Go HTTP-сервисов
Интерактивный курс Chi — роутера Go на net/http: маршруты, middleware, группы, контекст, JSON API, таймауты, graceful shutdown. 25 уроков с примерами и практикой.
- Строить маршруты chi: методы, параметры пути, wildcards
- Писать middleware и понимать порядок их выполнения
- Группировать маршруты: Route, Group и подроутеры
- Прокидывать данные в хендлеры через context.Context