ПОЧЕМУ стало ключевым словом, язык поднят до версии 2
- метка обоснования пишется заглавными и вошла в словарь набора: скелет правила теперь целиком из ключевых слов, а не смесь `**ДОЛЖЕН.**` и `**Почему.**`; в переводе на другой язык метка меняется как остальные слова (ПОЧЕМУ / WHY), 235 вхождений заменены - метки правила выделены из шкалы в отдельный перечень: ПОЧЕМУ и МЕХАНИЗИРОВАНО обязательности не задают, а размечают части, и стандартом не даются ни в одном языке — раньше МЕХАНИЗИРОВАНО висело строкой в таблице модальности - версия языка поднята до 2, потому что изменение формы меняет чтение уже написанного текста; строка о версии в двенадцати конвенциях перечисляет теперь и метки, а служебные слова сценария в неё по-прежнему не входят
This commit is contained in:
@@ -8,9 +8,9 @@ extends: arch/config.md
|
||||
Как базовый слой выглядит в Go-приложении: формат, загрузчик, границы
|
||||
запрета на окружение.
|
||||
|
||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и отметка
|
||||
МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 1 — тогда и
|
||||
только тогда, когда написаны заглавными.
|
||||
Ключевые слова ДОЛЖЕН, НЕ ДОЛЖЕН, СЛЕДУЕТ, НЕ СЛЕДУЕТ, ДОПУСКАЕТСЯ и метки
|
||||
ПОЧЕМУ и МЕХАНИЗИРОВАНО толкуются как описано в языке конвенций версии 2 —
|
||||
тогда и только тогда, когда написаны заглавными.
|
||||
|
||||
Секретов Go-специфика не касается: они приходят из деплоя уже в файле, а
|
||||
проверка их непустоты идёт вместе с остальной валидацией — как описано в
|
||||
@@ -22,7 +22,7 @@ extends: arch/config.md
|
||||
|
||||
**ДОЛЖЕН.** Конфиг — файл TOML.
|
||||
|
||||
**Почему.** Базовая конвенция оставляет формат за стеком, и этот выбор
|
||||
**ПОЧЕМУ.** Базовая конвенция оставляет формат за стеком, и этот выбор
|
||||
делается один раз на язык, а не в каждом приложении: разные форматы в
|
||||
соседних сервисах означают разные загрузчики, разные шаблоны рендера
|
||||
конфига в деплое и разное поведение при синтаксической ошибке. TOML при
|
||||
@@ -35,7 +35,7 @@ extends: arch/config.md
|
||||
**ДОЛЖЕН.** Чтение файла, наложение умолчаний и проверки живут в
|
||||
`internal/config`; наружу пакет отдаёт готовую структуру `Config`.
|
||||
|
||||
**Почему.** Пока значение не покинуло пакет, оно может быть невалидным;
|
||||
**ПОЧЕМУ.** Пока значение не покинуло пакет, оно может быть невалидным;
|
||||
после — уже нет, и это единственная граница, на которой такое утверждение
|
||||
проверяемо. Валидация, размазанная по потребителям, отвечает на вопрос
|
||||
«проверено ли это поле» только чтением всех вызывающих, часть полей
|
||||
@@ -48,7 +48,7 @@ extends: arch/config.md
|
||||
**ДОЛЖЕН.** Конфиг представлен одним значением типа `Config`, собранным из
|
||||
под-структур по секциям.
|
||||
|
||||
**Почему.** Один корень даёт одну точку, после которой конфиг проверен
|
||||
**ПОЧЕМУ.** Один корень даёт одну точку, после которой конфиг проверен
|
||||
целиком, и дальше передаётся как обычный аргумент. Несколько независимых
|
||||
структур конфига означают несколько загрузок и вопрос «какая из них уже
|
||||
провалидирована» на каждом использовании; связанные между собой поля
|
||||
@@ -59,7 +59,7 @@ extends: arch/config.md
|
||||
|
||||
**СЛЕДУЕТ.** Имя под-структуры совпадает с именем секции TOML.
|
||||
|
||||
**Почему.** Имя секции из файла — то, с чего начинают, когда конфиг ведёт
|
||||
**ПОЧЕМУ.** Имя секции из файла — то, с чего начинают, когда конфиг ведёт
|
||||
себя не так, как ожидали. При совпадении имён поиск по нему сразу приводит
|
||||
в код и обратно; при расхождении связь между полем файла и полем структуры
|
||||
восстанавливается чтением тегов, и проделывать это приходится для каждой
|
||||
@@ -70,7 +70,7 @@ extends: arch/config.md
|
||||
**ДОЛЖЕН.** Умолчания собраны в функции `Default()`, разобранный файл
|
||||
накладывается поверх.
|
||||
|
||||
**Почему.** Нулевое значение в Go неотличимо от «поле не задано»: нулевой
|
||||
**ПОЧЕМУ.** Нулевое значение в Go неотличимо от «поле не задано»: нулевой
|
||||
таймаут и отсутствующий таймаут — один и тот же `0`. Умолчание,
|
||||
подставленное по месту использования (`if x == 0 { x = … }`), поэтому не
|
||||
видно ни целиком, ни из образца, и два потребителя одного поля со временем
|
||||
@@ -83,7 +83,7 @@ extends: arch/config.md
|
||||
путь переопределяет флаг `--config=path`, образец рядом —
|
||||
`config.example.toml`.
|
||||
|
||||
**Почему.** Фиксированное имя и переопределение из командной строки требует
|
||||
**ПОЧЕМУ.** Фиксированное имя и переопределение из командной строки требует
|
||||
базовая конвенция, но конкретные имена она не задаёт. Одинаковые имена во
|
||||
всех сервисах означают, что unit-файл, `docker run` и инструкция по запуску
|
||||
пишутся, не открывая код приложения. Соседство `config.toml` и
|
||||
@@ -102,7 +102,7 @@ func (d *Duration) UnmarshalText(b []byte) error { … }
|
||||
func (d Duration) Std() time.Duration { … }
|
||||
```
|
||||
|
||||
**Почему.** `time.Duration` — это `int64`, и TOML разбирает его в голое
|
||||
**ПОЧЕМУ.** `time.Duration` — это `int64`, и TOML разбирает его в голое
|
||||
число: в конфиге остаётся `poll_interval = 5000`, о котором читатель не
|
||||
знает, секунды это, миллисекунды или наносекунды. Ошибка в тысячу раз
|
||||
проходит и разбор, и проверку диапазона, а проявляется нагрузкой или
|
||||
@@ -118,7 +118,7 @@ func (d Duration) Std() time.Duration { … }
|
||||
|
||||
**НЕ ДОЛЖЕН.** Значения конфигурации не берутся из переменных окружения.
|
||||
|
||||
**Почему.** Второй канал конфигурации — то, против чего написана базовая
|
||||
**ПОЧЕМУ.** Второй канал конфигурации — то, против чего написана базовая
|
||||
конвенция, но в Go он ещё и бесследный: `os.Getenv` вызывается из любого
|
||||
пакета, не появляется ни в `Config`, ни в образце и не проходит валидацию.
|
||||
Заведённый так параметр нельзя ни увидеть в конфиге, ни узнать иначе как
|
||||
@@ -134,7 +134,7 @@ func (d Duration) Std() time.Duration { … }
|
||||
^os\.(Getenv|LookupEnv|Environ|ExpandEnv)$
|
||||
```
|
||||
|
||||
**Почему.** `LookupEnv`, `Environ` и `ExpandEnv` читают ровно то же самое,
|
||||
**ПОЧЕМУ.** `LookupEnv`, `Environ` и `ExpandEnv` читают ровно то же самое,
|
||||
поэтому паттерн на одно имя оставляет три обхода. Хуже, что оставляет
|
||||
незаметно: правило числится механизированным, и глазами его больше никто не
|
||||
проверяет.
|
||||
@@ -155,7 +155,7 @@ func (d Duration) Std() time.Duration { … }
|
||||
| GCFG-10.1 | тесты, включая интеграционные | допустимо: креды и адреса внешних сервисов взять больше неоткуда |
|
||||
| GCFG-10.2 | Go-рантайм и ОС, а не наш код (`GOMEMLIMIT`, `GOMAXPROCS`, `GODEBUG`, `TZ`) | вне запрета: значение читает не приложение |
|
||||
|
||||
**Почему.** GCFG-8 — про конфигурацию приложения; расширенный до «никто не
|
||||
**ПОЧЕМУ.** GCFG-8 — про конфигурацию приложения; расширенный до «никто не
|
||||
трогает окружение», он запрещает то, чем не управляет: `GOMEMLIMIT` читает
|
||||
рантайм, а тесту креды внешнего сервиса взять неоткуда — в репозиторий их
|
||||
не положишь, а отдельный конфиг тестов пришлось бы заводить как ещё один
|
||||
@@ -167,7 +167,7 @@ func (d Duration) Std() time.Duration { … }
|
||||
|
||||
**ДОЛЖЕН.** Исходящий прокси приходит полем конфига и явным `Transport`.
|
||||
|
||||
**Почему.** `HTTP_PROXY`/`HTTPS_PROXY` формально читает не наш код, а
|
||||
**ПОЧЕМУ.** `HTTP_PROXY`/`HTTPS_PROXY` формально читает не наш код, а
|
||||
дефолтный `http.Transport` — но читает он их от имени приложения и меняет
|
||||
поведение приложения, а не рантайма. Оставленные окружению, они дают ровно
|
||||
тот второй канал, который запрещает GCFG-8, и притом самый неудобный: маршрут
|
||||
@@ -180,7 +180,7 @@ func (d Duration) Std() time.Duration { … }
|
||||
**ДОЛЖЕН.** Проверки не прерываются на первой неудаче, результат — одна
|
||||
ошибка, собранная `errors.Join`.
|
||||
|
||||
**Почему.** Возврат первой ошибки превращает починку конфига в серию
|
||||
**ПОЧЕМУ.** Возврат первой ошибки превращает починку конфига в серию
|
||||
перезапусков по одному полю за раз, причём каждый следующий запуск
|
||||
обнаруживает ещё одно. `errors.Join` даёт разом весь список, не требуя
|
||||
своего типа ошибки, и `errors.Is`/`errors.As` продолжают работать по каждой
|
||||
@@ -190,7 +190,7 @@ func (d Duration) Std() time.Duration { … }
|
||||
|
||||
**ДОЛЖЕН.** IANA-зона из конфига загружается на старте, в общей валидации.
|
||||
|
||||
**Почему.** Проверить имя зоны нечем, кроме загрузки: оно валидно ровно
|
||||
**ПОЧЕМУ.** Проверить имя зоны нечем, кроме загрузки: оно валидно ровно
|
||||
тогда, когда база зон его знает, и никакая проверка формата не отличит
|
||||
`Europe/Moscow` от `Europe/Moskow`. Без загрузки на старте опечатка
|
||||
доживает до первого форматирования времени — то есть до рантайма, мимо
|
||||
@@ -201,7 +201,7 @@ fail-fast (GCFG-15).
|
||||
**ДОЛЖЕН.** Импорт `_ "time/tzdata"` стоит в `main`, а не в библиотечном
|
||||
пакете.
|
||||
|
||||
**Почему.** Импорт в библиотеке навязывает ~450 КБ zoneinfo каждому
|
||||
**ПОЧЕМУ.** Импорт в библиотеке навязывает ~450 КБ zoneinfo каждому
|
||||
импортёру, включая тех, кому зоны не нужны: выбор «встраивать базу или
|
||||
полагаться на системную» принадлежит собираемой программе. Со встроенной
|
||||
базой ошибка `LoadLocation` (GCFG-13) означает ровно одно — битое имя зоны; без
|
||||
@@ -213,7 +213,7 @@ zoneinfo, а сообщение указывает не на ту причину
|
||||
**ДОЛЖЕН.** `main` пишет `slog` уровня `ERROR` и вызывает `os.Exit(1)` до
|
||||
старта серверов и воркеров.
|
||||
|
||||
**Почему.** Выход именно из `main`: `os.Exit` в библиотечном пакете не
|
||||
**ПОЧЕМУ.** Выход именно из `main`: `os.Exit` в библиотечном пакете не
|
||||
оставляет вызывающему возможности ни залогировать причину, ни дописать
|
||||
контекст, и отложенные `defer` при нём не выполняются вовсе. Выход именно
|
||||
до старта воркеров: горутина, поднятая раньше валидации, успевает сходить
|
||||
|
||||
Reference in New Issue
Block a user