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

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

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

Урок 5 из 25 ~16 мин +20 XP

SetDefault: значения по умолчанию

Viper SetDefault задаёт значения по умолчанию — нижний слой реестра, благодаря которому программа стартует без единого файла конфига. Разберём, как дефолты документируют схему настроек, почему IsSet считает их установленными, что показывает AllSettings и где тип дефолта превращает таймаут в 30 наносекунд — на сервисе, который обязан запускаться с пустой папкой.

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

Свежий клон проекта: git clone, go run . — и программа падает на нулевом порте или уходит в вечное ожидание на нулевом таймауте. Классика: config.yaml в репозиторий не положили, потому что когда-то туда вписали пароль и отправили файл в .gitignore, а код рассчитан, что файл есть. Выкатка в новый контур повторяет ту же историю: контейнер стартует без файла, ReadInConfig возвращает ошибку, которую кто-то проглотил, и сервис поднимается с нулями вместо настроек. Дефолты конфига существуют ровно для этого сценария: viper.SetDefault объявляет значение по умолчанию для каждого ключа, и программа без файла конфига обязана запускаться — на разумных значениях, а не на нулях. Это самый нижний слой реестра: его перекрывает всё — файл, окружение, флаги, — но без него пустой запуск означает пустую конфигурацию и падение в самый неподходящий момент.

viper SetDefault: значения по умолчанию#

Сигнатура простая: viper.SetDefault(ключ, значение). Ключ — та же строка с точками, что и в Get: server.port, database.host, worker.retries. Значение — обычное Go-значение любого типа: число, строка, булево, срез, карта, time.Duration. Вызов делается в любой момент до чтения ключа: viper хранит дефолты в собственном слое реестра и подмешивает их только там, где сильные источники промолчали. Отсюда два следствия. Первое: дефолт никогда не перекроет файл, окружение или флаг — он честный запасной вариант, а не способ задать настройку из кода. Второе: чтение до SetDefault вернёт ноль — если GetInt выполнится раньше, чем дефолт записан, слой ещё пуст, и повторного пересчёта не будет, пока ключ не прочитан заново. На практике все SetDefault собирают в одну функцию и вызывают первой строкой main, до файлов и окружений — о порядке чуть ниже.

Программа, которой файл не нужен

GO
// config.yaml не существует — программа всё равно стартует
viper.SetDefault("server.host", "0.0.0.0")
viper.SetDefault("server.port", 8080)
viper.SetDefault("server.debug", false)
viper.SetDefault("worker.retries", 3)

fmt.Println(viper.GetString("server.host")) // 0.0.0.0
fmt.Println(viper.GetInt("server.port"))    // 8080
fmt.Println(viper.GetBool("server.debug"))  // false
fmt.Println(viper.GetInt("worker.retries")) // 3

Ни SetConfigFile, ни ReadInConfig — реестр наполнен только дефолтами, и все четыре ключа читаются штатно. Такой запуск — минимальный контракт сервиса: он всегда поднимается, а внешние источники лишь уточняют значения. Уберите один SetDefault — и соответствующее чтение молча вернёт нулевое значение типа.

Дефолты как документация схемы конфига#

У SetDefault есть вторая роль, которую замечают через месяц работы: блок дефолтов — это исполняемая документация схемы конфигурации. Открыв начало main, новый разработчик видит все ключи, их типы и разумные значения — вместо чтения README или реверс-инжиниринга чужого конфига. Из этой роли вытекает поведение IsSet: она возвращает true для ключа, у которого задан дефолт, — с точки зрения реестра ключ существует, просто значение пришло из нижнего слоя. Связку IsSet с чтением мы разбирали в прошлом уроке: там она отличала «нет ключа» от «значение 0»; с дефолтами первая ситуация исчезает, и IsSet превращается в проверку «задал ли кто-то снаружи своё значение». Для необязательных настроек дефолтом может быть и пустая строка: ключ объявлен, а значение выберут окружение или файл.

Дефолты в AllKeys и AllSettings

GO
viper.SetDefault("server.port", 8080)
viper.SetDefault("log.level", "info")

fmt.Println(viper.AllKeys())
// [server.port log.level] — порядок строк не гарантирован

fmt.Println(viper.AllSettings())
// map[log.level:info server.port:8080]

fmt.Println(viper.IsSet("server.port")) // true

Дефолт не писался ни в один файл, но AllKeys и AllSettings его показывают: реестр единый, и слои снаружи неразличимы. Дамп на старте — быстрый способ увидеть, с какими значениями поедет программа, пока внешних источников нет. Проверьте это локальным Go: go run main.go без всяких файлов — песочница в браузере сторонние библиотеки не запускает.

Здесь есть тонкая грань, о которую спотыкаются при отладке. Дефолт не появляется ни в config.yaml, ни в выводе WriteConfig: файл после чтения остаётся прежним, а значение из дефолта тем временем честно возвращают Get и AllSettings. Ломается это на диагностике: инженер видит в дампе server.port: 8080, открывает файл, находит там 9000 — и делает вывод, что viper прочитал не тот файл. А на самом деле файл в этом запуске вообще не читался: ReadInConfig вернул ошибку, её проглотили, и реестр остался на дефолтах. Значение в дампе — не признак источника, а только текущее состояние реестра. Лечится дисциплиной: ошибка ReadInConfig печатается вместе с именем файла, а рядом с дампом логируется факт, читался ли конфиг вообще.

Тип дефолта: число, строка и длительность#

SetDefault кладёт в реестр ровно то значение, которое вы передали, вместе с его типом, а чтение приводится по правилам cast из урока про семейство Get. Для чисел и строк сюрпризов нет: SetDefault("server.port", 8080) читается GetInt-ом как есть, GetString вернёт "8080", GetBool справится с true и false. А вот длительности — место, где дефолты регулярно стреляют. Число 30 в реестре — это int, и GetDuration трактует целые числа как наносекунды: таймаут из дефолта истекает мгновенно. Строка "30s" парсится time.ParseDuration и даёт честные тридцать секунд, а литерал 30*time.Second кладёт в реестр сразу готовую длительность. В конфиг-файле ту же роль играет кавычка: timeout: "30s". Правило умещается в одну строку: тип дефолта задавайте тем типом, которым планируете читать, а длительности — строкой с суффиксом.

Тип дефолта и что вернёт чтение#

ДефолтЧтениеРезультат
SetDefault("server.port", 8080)GetInt8080
SetDefault("app.name", "local")GetStringlocal
SetDefault("server.debug", false)GetBoolfalse
SetDefault("timeout", 30)GetDuration30ns — число стало наносекундами
SetDefault("timeout", "30s")GetDuration30s
SetDefault("tags", []string{"go", "api"})GetStringSlice[go api]

Три варианта одного таймаута

GO
viper.SetDefault("timeout.bad", 30)               // int
viper.SetDefault("timeout.good", "30s")           // строка с суффиксом
viper.SetDefault("timeout.typed", 30*time.Second) // time.Duration

fmt.Println(viper.GetDuration("timeout.bad"))   // 30ns
fmt.Println(viper.GetDuration("timeout.good"))  // 30s
fmt.Println(viper.GetDuration("timeout.typed")) // 30s

Один смысл — три записи. Числовой дефолт попадает в реестр как int, и cast не додумывает единицы измерения за вас: 30 становится наносекундами. Строка с суффиксом парсится time.ParseDuration, типизированный литерал проходит как есть. Для компиляции понадобится пакет time; строки вида "45m" и "1h30m" тоже сработают.

Кто сильнее дефолта: приоритет слоёв#

Дефолт — нижняя ступень лестницы приоритетов из первого урока: Set, флаг, окружение, конфиг-файл и key/value-хранилище перекрывают его молча и безусловно. Важная деталь: сила слоя не зависит от порядка вызовов. SetDefault до ReadInConfig или после — результат одинаков: файл всё равно победит, потому что при чтении ключа viper спускается по слоям сверху вниз и останавливается на первом непустом. Порядок вызовов влияет ровно на один случай — чтение раньше записи. GetInt, выполненный до SetDefault, вернёт 0: слой ещё пуст. Поэтому дефолты ставят до любых чтений: сначала весь блок SetDefault, потом файл, потом окружение, и только затем рабочий код берёт значения. Так стартовая секция программы читается как оглавление: схема, источники, логика.

Гигиена дефолтов#

  • Дефолт есть у каждого ключа, который упомянут в README или в примере конфига: схема документируется кодом, а не вики.
  • Все SetDefault собраны в одной функции на старте: initDefaults первой строкой main, до файлов, окружения и чтений.
  • Тип дефолта равен типу чтения: длительности — строкой 30s или литералом time.Duration, порты — числом.
  • Пустая строка — легальный дефолт необязательной настройки: ключ объявлен, значение выберут окружение или файл.
  • Секретов в дефолтах нет: SetDefault("db.password", ...) компилируется в бинарник и всплывает в AllSettings.
  • Дефолт — безопасный старт, а не боевая конфигурация: не копируйте в него продовые значения.

Проверка: IsSet и дефолты

Для ключа server.port задан только viper.SetDefault("server.port", 8080). Файла конфига нет, переменных окружения нет. Что вернёт viper.IsSet("server.port")?

Проверка: файл против дефолта

В config.yaml написано port: 9000, а в коде есть SetDefault("server.port", 8080) — до и после ReadInConfig. Что вернёт viper.GetInt("server.port")?

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

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

viper.SetDefault("timeout", 30)
fmt.Println(viper.GetDuration("timeout"))

Соберите запуск без файла

Допишите дефолт, проверку ключа и чтение значения — программа должна стартовать без config.yaml.

viper.("server.port", 8080) viper.SetDefault("log.level", "info") if viper.("server.port") { port := viper.("server.port") fmt.Println(port) } fmt.Println(viper.AllSettings())

Соберите стартовую секцию программы

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

Строки

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

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

Карточки: значения по умолчанию

Термин

SetDefault

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

viper.SetDefault(ключ, значение) — нижний слой реестра: значение по умолчанию, которое перекрывается файлом, окружением, флагами и Set. Ключи с точками: server.port.

1 / 6

Что дальше#

Дефолты гарантируют старт, но если каждый контур деплоя будет уточнять значения правкой своего файла, схема превратится в три копии config.yaml, которые расползутся за неделю. Следующая ступень — окружение: AutomaticEnv учит реестр отвечать значениями из переменных деплоя, перекрывая и файл, и дефолт, — стандартный канал для systemd, Docker и Kubernetes. После неё — префиксы против коллизий, флаги и большая тема приоритетов источников. Пока же соберите стартовую секцию своего проекта локальным Go: закомментируйте SetConfigFile, запустите с пустой папкой и посмотрите на дамп AllSettings — три минуты практики показывают всю механику дефолтов лучше любого текста. Маршрут по остальным темам курса — на странице viper.

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

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

Вывод

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

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

  • SetDefault — нижний слой реестра: программа без файла конфига стартует на дефолтах, а внешние источники только уточняют значения.
  • Сила слоя не зависит от порядка вызовов: файл перекрывает дефолт и до, и после ReadInConfig; порядок важен лишь для чтений — Get до SetDefault вернёт ноль.
  • Блок дефолтов — исполняемая документация схемы: ключи, типы и разумные значения видны на первом экране main.
  • Дефолт не появляется в файле, но виден в AllKeys и AllSettings; IsSet возвращает true и для дефолтов.
  • Тип дефолта важен: SetDefault("timeout", 30) даёт GetDuration 30 наносекунд; длительности задавайте строкой "30s" или time.Duration.
  • Все SetDefault собирают в initDefaults первой строкой main — не размазывают по коду.
  • Секреты в дефолтах запрещены: они попадают в бинарник и в дамп AllSettings.

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

Соберите initDefaults для своего проекта: пять ключей разных типов, включая длительность строкой и срез строк. Запустите программу без config.yaml и напечатайте AllSettings, затем создайте файл с одним перекрытым ключом и убедитесь, что остальные остались на дефолтах. Всё — локальным Go: песочница файлов не имеет.

Личные заметкиПусто
Хранятся только в вашем браузере (Viper · SetDefault: значения по умолчанию)

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

Нужно ли задавать дефолты, если конфиг-файл гарантированно есть?

Да, по двум причинам. Первая — запуск из свежего клона или нового контура, где файла ещё нет: без дефолтов это падение на нулях. Вторая — документация: блок SetDefault перечисляет схему настроек с типами и разумными значениями, и его читают чаще, чем README.

SetDefault перекрывает значение из файла?

Никогда. Дефолт — нижний слой лестницы приоритетов, файл стоит выше. Даже если SetDefault вызван после ReadInConfig, файл победит: сила слоя зашита в библиотеку, а не определяется порядком вызовов. Если нужно перекрыть всё из кода, существует Set — но это инструмент тестов, а не настройки.

Можно ли сделать дефолтом пустую строку или ноль?

Легально: SetDefault("api.key", "") объявляет необязательный ключ, IsSet вернёт true, а валидация на старте решит, критично ли пустое значение. Нулевые дефолты применяйте осознанно: порт 0 отправит сервер на случайный порт, а таймаут 0 у многих клиентов означает «ждать вечно».

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

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

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

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

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

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

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