---
title: "WriteConfig и SafeWriteConfig: сохраняем состояние"
description: "viper WriteConfig сохраняет реестр на диск: WriteConfigAs, SafeWriteConfig и WriteConfigTo в io.Writer. Куда пишет каждый метод и когда запись опасна."
course: "Viper: конфигурация приложений на Go"
url: "https://golangmaster.ru/courses/viper/lessons/13-zapis-configa/"
published: "2026-09-19"
updated: "2026-09-27"
minutes: 16
---

# WriteConfig и SafeWriteConfig: сохраняем состояние

*Курс «Viper: конфигурация приложений на Go», урок 13 из 25. ~16 минут. Обновлено 27.09.2026. Источник: https://golangmaster.ru/courses/viper/lessons/13-zapis-configa/*

viper WriteConfig сохраняет текущий реестр — Set-ключи, значения файла и дефолты — обратно на диск, а SafeWriteConfig отказывается перезаписывать существующий конфиг. Запись в проде чаще вредит, чем помогает, зато незаменима в командах init и export. Разберём пять методов, что именно попадёт в файл и почему комментарии там не выживут.

Программа прочитала 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](/courses/viper/lessons/05-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: только в несуществующий файл

```go
func 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, и проверка записи готова.

### Экспорт настроек в буфер

```go
viper.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](/courses/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() вернула ошибку. Почему?

- ✅ WriteConfig пишет в найденный файл: файла нет — путь не определён, и запись падает
- ReadInConfig обязателен перед любой записью: без чтения viper запрещает WriteConfig
- SetConfigType нельзя вызывать до ReadInConfig
- Ошибка означает, что WriteConfig вообще не возвращает error

Цель записи вычисляется поиском по путям: файла нет — писать некуда, и getConfigFile вернёт ошибку. При этом ReadInConfig перед записью не обязателен — реестр может состоять из одних дефолтов. Альтернатива — SafeWriteConfig, который при заданных путях и типе создаст файл с нуля.

### Проверка: SafeWriteConfig на живом файле

**Вопрос:** config.yaml существует и содержит живые настройки пользователя. Что сделает SafeWriteConfig() при заданных путях поиска?

- Перезапишет файл дампом реестра
- ✅ Вернёт ErrConfigFileAlreadyExists и не тронет содержимое
- Допишет новые ключи в конец файла
- Создаст резервную копию, а затем перезапишет файл

Safe-варианты пишут только в несуществующий файл: уже созданный — это ошибка ErrConfigFileAlreadyExists, и содержимое гарантированно не меняется. Ни дозаписи, ни слияния ключей, ни бэкапов: намерение «не повредить существующее» выражено самим методом.

### Что окажется в файле?

```go
// configs/config.yaml до запуска: server: {host: "old.local"}

viper.SetConfigFile("configs/config.yaml")
_ = viper.ReadInConfig()
viper.Set("server.host", "new.local")
_ = viper.WriteConfig()
```

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

- ✅ server: {host: new.local} — в дамп попадают и Set-значения, реестр перезаписал файл
- server: {host: old.local} — Set живёт только в памяти и на диск не пишется
- Файл не изменится: WriteConfig вернёт ошибку про существующий файл
- Файл станет пустым: без SetDefault реестр пуст

WriteConfig сериализует всё, что видит AllSettings, включая ключи из Set: old.local заменён на new.local. Именно поэтому в сервисах, чьи конфиги лежат в git, запись на диск запрещают: правки процесса затирают документ, который ведут люди.

### Соберите три сценария записи

Допишите методы: дамп в буфер, защищённая запись по пути и запись в найденный файл конфига.

```go
// шаблон конфига для команды init — без записи на диск
buf := new(bytes.Buffer)
if err := viper.___(buf); err != nil {
        log.Fatal(err)
}

// создать config.yaml, только если его ещё нет
if err := viper.___("config.yaml"); err != nil {
        log.Fatal(err)
}

// сохранить реестр в найденный файл конфига
if err := viper.___(); err != nil {
        log.Fatal(err)
}
```

Подсказка: В буфер пишет WriteConfigTo, защищённая запись по указанному пути — SafeWriteConfigAs, запись в найденный файл — WriteConfig.

Правильные ответы в пропусках: `WriteConfigTo`, `SafeWriteConfigAs`, `WriteConfig`

### Соберите команду init по строкам

Расставьте строки: два дефолта, один Set, защищённая запись в новый файл и подтверждение в выводе.

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

1. `viper.SetDefault("server.host", "0.0.0.0")`
2. `viper.SetDefault("server.port", 8080)`
3. `viper.Set("worker.timeout", "30s")`
4. `err := viper.SafeWriteConfigAs("config.yaml")`
5. `if err != nil { log.Fatal("config.yaml уже существует:", err) }`
6. `fmt.Println("создан config.yaml с настройками по умолчанию")`

### Карточки: запись конфига

- **WriteConfig** — Дамп всего реестра — файл, Set, дефолты — в найденный или заданный файл конфига. Перезаписывает молча и целиком; комментарии и порядок ключей не сохраняются.
- **SafeWriteConfig** — Та же запись, но только в несуществующий файл: уже созданный даёт ErrConfigFileAlreadyExists и не трогается. Основа команды init.
- **WriteConfigAs** — Запись в путь из аргумента; формат определяется расширением, иначе нужен SetConfigType. Есть Safe-вариант с защитой от перезаписи.
- **WriteConfigTo** — Дамп в io.Writer: буфер, stdout, открытый файл. Единственный метод, который сам не трогает диск; тип формата берётся из SetConfigType.
- **Что попадает в дамп** — AllSettings: значения файла, ключи из Set, дефолты, явно привязанные env и флаги. AutomaticEnv не перечисляется и в дамп не попадает.
- **Почему теряются комментарии** — Реестр — карта значений: viper маршалит map[string]any, а исходный текст файла нигде не хранится. Шаблон с комментариями держите отдельной заготовкой.

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

Запись на диск закрыта, и следующий урок — [Sub: поддеревья конфига](/courses/viper/lessons/14-sub-derevya/): общий реестр разрезается на изолированные срезы для модулей. Полный маршрут по темам лежит на [странице курса viper](/courses/viper/). Домашний опыт — локальным Go: соберите команду init из урока, добейтесь, чтобы второй запуск не трогал существующий config.yaml, а дамп WriteConfigTo в тесте совпал с ожидаемым yaml. Заодно проверьте собственный код: нет ли в нём WriteConfig, который молча перезаписывает файл, собранный руками.

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

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