Патерни конфігурації¶
Конфігурація, розкидана по кодовій базі як виклики os.Getenv, не має
жодного єдиного місця для читання, жодної перевірки, а описка стає
нульовим значенням замість помилки. Одна структура, завантажена й
перевірена один раз при старті, виправляє всі три проблеми.
type Config struct {
Environment string `env:"ENVIRONMENT"`
Port int `env:"PORT"`
Timeout time.Duration `env:"TIMEOUT"`
DBHost string `env:"DB_HOST"`
}
Одна структура, завантажена один раз¶
Правила, що роблять це робочим:
- Завантажуйте в
main, перш ніж запуститься щось інше. - Передавайте структуру вниз як параметр конструктора. Ніколи як глобальну змінну пакета, інакше ви повертаєтесь до невидимих залежностей.
- Провалюйтесь при старті, а не на першому запиті. Відсутній хост бази даних має зупинити процес негайно, поки хтось за цим стежить.
Давайте полям справжні типи. Timeout time.Duration замість
TimeoutSeconds int означає, що розбір і одиниця виміру живуть в
одному місці, а не в кожному місці використання.
Спершу значення за замовчуванням, потім перевизначення¶
Починайте з заповненої структури, щоб кожне поле мало розумне значення, а тоді дайте середовищу перевизначити його:
func Load() (Config, error) {
c := Config{
Environment: "local",
Port: 8080,
Timeout: 5 * time.Second,
}
// ... застосувати середовище ...
}
Значення за замовчуванням у літералі структури самодокументовані: одне місце показує, що робить програма взагалі без жодної конфігурації.
Прив'язка через рефлексію¶
Для жменьки полів явні виклики os.LookupEnv цілком доречні й
зрозуміліші. Коли їх стає тридцять, повторення саме стає джерелом
багів, і обхід тегів структури вартий того — техніка reflect зі
статті про
XML, CSV і рефлексію:
v := reflect.ValueOf(&c).Elem()
t := v.Type()
for i := range t.NumField() {
tag := t.Field(i).Tag.Get("env")
if tag == "" || tag == "-" {
continue
}
raw, ok := os.LookupEnv(tag)
if !ok {
continue // залишити значення за замовчуванням
}
f := v.Field(i)
switch f.Interface().(type) {
case string:
f.SetString(raw)
case int:
n, err := strconv.Atoi(raw)
if err != nil {
return Config{}, fmt.Errorf("%s: %w", tag, err)
}
f.SetInt(int64(n))
case time.Duration:
d, err := time.ParseDuration(raw)
if err != nil {
return Config{}, fmt.Errorf("%s: %w", tag, err)
}
f.SetInt(int64(d))
}
}
reflect.ValueOf(&c).Elem() — це те, що робить поля придатними для
запису. ok від LookupEnv відрізняє "не встановлено" від
"встановлено як порожнє", тож невстановлена змінна лишає значення за
замовчуванням, а не обнуляє його.
Зверніть увагу, що помилка включає ім'я змінної:
env:"-" позначає поле, яке завантажувач має пропустити — типово те,
що виводиться з інших.
Це точно те, що роблять бібліотеки конфігурації. Знання механізму означає, що ви можете прочитати їхню, і вирішити, чи сорок рядків кращі за залежність.
Похідні поля¶
Обчислюйте все, зібране з інших значень, один раз, після завантаження:
Ще краще як метод, щоб воно не могло розійтися з реальністю:
У будь-якому разі будуйте його в одному місці. DSN, зібраний у трьох місцях виклику, відрізнятиметься у двох із них.
Перевіряйте й повідомляйте все одразу¶
Повернення на першій проблемі означає виправлення однієї змінної на
перезапуск. errors.Join збирає їх усі:
func (c Config) validate() error {
var errs []error
if c.DBHost == "" {
errs = append(errs, errors.New("DB_HOST is required"))
}
if c.Port < 1 || c.Port > 65535 {
errs = append(errs, fmt.Errorf("PORT %d out of range", c.Port))
}
return errors.Join(errs...)
}
errors.Join повертає nil, коли зріз порожній, тож щасливий шлях не
потребує особливого випадку.
// нічого не встановлено
// output: DB_HOST is required
// PORT=99999
// output: PORT 99999 out of range
Перевіряйте також діапазони й формати, а не лише наявність. Розмір пулу нуль чи поріг подібності 1.5 інакше провалиться десь далеко від причини.
Секрети¶
Змінні середовища — звичайний транспорт, і вони легко просочуються. Дві звички:
- Ніколи не логуйте структуру конфігурації.
%+vна ній кладе пароль вашої бази даних у логи. Якщо хочете зведення при старті, напишіть методRedacted()або дайте секретним полямString(), що повертає"[redacted]". - Не кладіть секрети у значення за замовчуванням. Пароль за замовчуванням у коді — це пароль у вашій git-історії.
Файли .env — це зручність локальної розробки. Стандартна бібліотека
їх не читає; це турбота сторонньої бібліотеки, а продакшн має
використовувати справжні змінні середовища чи сховище секретів.
Прапорці, середовище чи файл¶
| Джерело | Добре для |
|---|---|
| середовище | конфігурація розгортання: хости, облікові дані, feature flags |
| прапорці | вибір для конкретного запуску: --mode, --dry-run, шлях до файлу |
| файл | велика чи структурована конфігурація, закомічена в репозиторій |
Середовище — типове джерело для сервісів, бо саме це надають оркестратори. Звичний порядок — значення за замовчуванням, потім файл, потім середовище, потім прапорці — кожен перевизначає попередній. Що б ви не обрали, задокументуйте це, бо інакше на питання "чому це значення не застосовується" відповіді не буде.
З досвіду Python: це pydantic-settings, зроблений вручну. Структура — це схема, теги — псевдоніми полів, а перевірка — метод, який ви пишете. Менше магії, і все вміщується в одну зрозумілу функцію.
Швидка довідка¶
| Питання | Форма |
|---|---|
| форма | одна структура, справжні типи (time.Duration, не int) |
| коли | один раз у main, передається вниз як параметр |
| значення за замовчуванням | заповнений літерал структури перед перевизначеннями |
| не встановлено проти порожнє | os.LookupEnv, не os.Getenv |
| багато полів | обхід тегів структури через reflect |
| пропустити поле | env:"-" |
| похідні значення | один метод, а не повтор у місцях виклику |
| перевірка | збирати через errors.Join, провалюватися при старті |
| секрети | ніколи не логувати структуру; жодних секретних значень за замовчуванням |