- 13 конвенций по осям arch / lang / stack / common; репозитории берут оттуда копии в свой docs/conventions/ и коммитят их у себя - conv — синхронизация копий: add / status / diff / pull / push, локальные регионы исключены из сравнения, поэтому расхождение не даёт шума
90 lines
4.4 KiB
Markdown
90 lines
4.4 KiB
Markdown
---
|
||
status: рекомендуемая
|
||
extends: arch/config.md
|
||
---
|
||
|
||
# Конфигурация: реализация на Go
|
||
|
||
Как `arch/config.md` выглядит в Go-приложении.
|
||
|
||
## Формат и загрузчик
|
||
|
||
- TOML. Разбор и валидация — целиком в `internal/config`; наружу отдаётся
|
||
готовая структура `Config`.
|
||
- Одна корневая структура `Config` с под-структурами по секциям — имена
|
||
структур совпадают с именами секций, чтобы конфиг и код читались рядом.
|
||
- Умолчания — в `Default()`, поверх накладывается разобранный файл.
|
||
- Флаг `--config=path` переопределяет путь; по умолчанию `config.toml` в
|
||
рабочей директории, образец — `config.example.toml`.
|
||
|
||
## Длительности
|
||
|
||
`time.Duration` не разбирается из строки TOML сама по себе — нужен свой тип
|
||
с `UnmarshalText`, отдающий `time.Duration`:
|
||
|
||
```go
|
||
type Duration time.Duration
|
||
|
||
func (d *Duration) UnmarshalText(b []byte) error { … }
|
||
func (d Duration) Std() time.Duration { … }
|
||
```
|
||
|
||
Так в конфиге видна единица измерения (`poll_interval = "5s"`), а не голое
|
||
число. Цена: ошибка в длительности всплывает **на разборе TOML**, до общей
|
||
валидации, поэтому в общий сбор проблем она не попадает — про неё узнаёшь
|
||
отдельно и первой.
|
||
|
||
## Чтение окружения
|
||
|
||
Приложение не читает окружение для конфигурации. Механизируется
|
||
`forbidigo`, и паттерн должен покрывать **все** входы, а не только
|
||
`os.Getenv`:
|
||
|
||
```
|
||
^os\.(Getenv|LookupEnv|Environ|ExpandEnv)$
|
||
```
|
||
|
||
Правило про приложение, поэтому за его границей запрет не действует:
|
||
|
||
- **тесты** — не приложение: интеграционному тесту нормально брать
|
||
креды внешнего сервиса из окружения;
|
||
- **переменные рантайма** — те, что читает не наш код, а Go или ОС
|
||
(`GOMEMLIMIT`, `GOMAXPROCS`, `GODEBUG`, `TZ`).
|
||
|
||
Отдельный случай — переменные, которые читает **стандартная библиотека от
|
||
имени приложения**: дефолтный `http.Transport` уважает
|
||
`HTTP_PROXY`/`HTTPS_PROXY`. Формально их читает не наш код, но это
|
||
конфигурация поведения приложения, поэтому прокси задаётся полем конфига и
|
||
явным `Transport`, а не окружением.
|
||
|
||
## Валидация
|
||
|
||
- Проверки собираются `errors.Join`, чтобы за один запуск показать **все**
|
||
проблемы конфига, а не первую.
|
||
- IANA-зона валидируется `time.LoadLocation`. База зон встраивается
|
||
импортом `_ "time/tzdata"` **в `main`**, а не в библиотечном пакете:
|
||
иначе ~450 КБ zoneinfo навязываются каждому импортёру. Со встроенной
|
||
базой ошибка `LoadLocation` означает битое имя зоны, а не отсутствие
|
||
zoneinfo в контейнере.
|
||
- Невалидный конфиг — `slog` уровня `ERROR` и `os.Exit(1)` из `main`, до
|
||
старта серверов и воркеров.
|
||
|
||
## Секреты
|
||
|
||
Go-специфики нет: секреты приходят из деплоя уже в файле, проверка их
|
||
непустоты идёт вместе с остальной валидацией — см. базу.
|
||
|
||
<!-- local:поля -->
|
||
<!-- /local -->
|
||
|
||
<!-- local:механизировано -->
|
||
<!-- /local -->
|
||
|
||
## Связано
|
||
|
||
- `lang/go/time.md` — зона отображения и формат времени.
|
||
- `lang/go/logging.md` — `slog`, которым падает невалидный конфиг.
|
||
|
||
<!-- local:связано -->
|
||
<!-- /local -->
|