---
title: "Конфигурация сервисов"
description: "Конфигурация микросервисов: флаги и env-переменные, безопасные дефолты, валидация с fail fast, viper и секреты, сборка Config в композиционном корне."
course: "go-kit: микросервисный тулкит"
url: "https://golangmaster.ru/courses/go-kit/lessons/18-konfiguraciya/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Конфигурация сервисов

*Курс «go-kit: микросервисный тулкит», урок 18 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-kit/lessons/18-konfiguraciya/*

Композиционный корень из урока 17 собран, но в нём остались зашитые значения: порты :8081 и :8082, адрес Consul, доля сэмплинга Jaeger. Собранный бинарь живёт ровно в одной среде — на твоей машине, а пересобирать артефакт ради стейджинга — антипаттерн, который ломает и аудит, и откаты. Тема урока — конфигурация микросервисов: ты вынесешь все значения наружу через флаги и env-переменные, соберёшь их в одну структуру Config, добавишь безопасные дефолты, валидацию с fail fast и честно поговорим про viper и секреты. К концу урока один и тот же go-артефакт будет запускаться на ноутбуке, в CI и в проде без единой правки кода — а конфиг go сервиса будет собирать ровно одно место: композиционный корень.

### Один артефакт, много сред

Пройди по проекту глазами конфигуратора: транспорт из [урока 04-http-transport](/courses/go-kit/lessons/04-http-transport/) слушает ":8081", gRPC из урока 13 — ":8082", consul-регистратор из [урока 12-discovery](/courses/go-kit/lessons/12-discovery/) ходит в "localhost:8500", Jaeger-сэмплер из [урока 08-tracing](/courses/go-kit/lessons/08-tracing/) трассирует с вероятностью из константы. Каждая строка — решение среды, спрятанное в код. Twelve-Factor, свод правил, по которому живут контейнерные платформы, формулирует это в пункте III — фундаменте конфигурации микросервисов: конфигурация хранится в окружении, а не в коде — один и тот же артефакт продвигается от дева к проду, меняя только окружение. В go-kit эта идея ложится без трения, потому что место для неё уже приготовлено: раскладка из урока 17 отвела сборку зависимостей композиционному корню, а [урок 03-service](/courses/go-kit/lessons/03-service/) запретил читать env в глубине методов. Конфигурация — это не слой сервиса и не библиотечный модуль: это значения, которыми main кормит конструкторы. Отсюда правило урока: env go-сервис читает в одном месте — в main, и дальше по проекту едут уже обычные значения.

### Три источника и приоритет

Практика выработала три слоя источников, и у каждого своя роль. Дефолты в коде — то, что работает без всякой настройки: локальный запуск go run ./cmd/stringsvc должен поднимать рабочий сервис, не требуя ни одной переменной. Переменные окружения — язык контейнеров и оркестраторов: Kubernetes, docker run и CI передают настройки именно env, поэтому env go-сервиса — основной канал продовых значений. Флаги командной строки — язык локального запуска и разовых экспериментов: go run ./cmd/stringsvc -http-addr=:9090 читается лучше, чем экспорт переменной. Приоритет вытекает из способа чтения: сначала берётся дефолт, потом env перезаписывает его, если задан, потом флаг перезаписывает всё, если задан явно — что задано явнее, то и побеждает. Такой порядок даёт приятное свойство: прод задан в окружении, а разработчик может точечно переопределить один флаг, не трогая остальное. Важно и обратное: не плодить источники сверх трёх — файлы конфигурации добавим отдельным разговором, когда поймём, зачем они нужны, а не потому что «так принято».

### Config и флаги: одна структура на сервис

```go
// internal/config/config.go — единственный пакет, знающий про окружение.
type Config struct {
        HTTPAddr   string  // слушатель HTTP из урока 04
        GRPCAddr   string  // слушатель gRPC из урока 13
        ConsulAddr string  // агент Consul из урока 12
        SampleRate float64 // вероятность сэмплинга Jaeger из урока 08
        LogLevel   string
}

func Flags() (Config, error) {
        var c Config
        flag.StringVar(&c.HTTPAddr, "http-addr", ":8081", "HTTP listen address")
        flag.StringVar(&c.GRPCAddr, "grpc-addr", ":8082", "gRPC listen address")
        flag.StringVar(&c.ConsulAddr, "consul-addr", "localhost:8500", "Consul agent")
        flag.Float64Var(&c.SampleRate, "sample-rate", 0.01, "Jaeger sampling probability")
        flag.StringVar(&c.LogLevel, "log-level", "info", "log level")
        flag.Parse()

        if err := c.validate(); err != nil {
                return Config{}, err
        }
        return c, nil
}
```

Вся конфигурация сервиса — одна структура и один конструктор: поле с дефолтом, обращённым в прошлые уроки (порты 04 и 13, Consul 12, сэмплер 08). flag.Parse читает аргументы командной строки; каждый флаг получает дефолт, совпадающий с локальной конвенцией курса, — так безопасный дефолт возникает сам собой. validate() вызывается прямо в конструкторе: невалидная сборка не выходит из пакета config — об этом ниже. Структура возвращается значением, не указателем: Config после Parse больше не меняется, и передавать его по проекту безопасно.

### env-переменные и префиксы

env-канал строится поверх дефолтов: каждая переменная переопределяет значение, если задана. Штатный способ — os.LookupEnv вместо os.Getenv: LookupEnv различает «переменная пустая» и «переменной нет», а os.Getenv возвращает пустую строку в обоих случаях, и ты не отличишь явную настройку от её отсутствия. Формула чтения простая: если есть — взять, нет — оставить дефолт. Чтобы переменные одного сервиса не столкнулись с чужими, каждый сервис носит префикс: STRINGSVC_HTTP_ADDR, STRINGSVC_CONSUL_ADDR — конвенция «ПРОПИСНЫЕ_С_ПОДЧЁРКИВАНИЯМИ», которую оркестраторы понимают из коробки. Не выдумывай формат значений: порты и адреса храни строками до самого listener, проценты — числами, списки — через запятую; парсинг делает один хелпер, а не каждый вызов в проекте. Мост к канону курса: сервис и endpoint'ы не видят ни env, ни префиксов — они получают уже разобранные значения через конструкторы из урока 17; тест из урока 03 снова собирается одной строкой в чистой среде, и никакая среда не меняет поведение сервиса незаметно.

### env-хелперы поверх дефолтов

```go
const prefix = "STRINGSVC_"

func envString(key, def string) string {
        if v, ok := os.LookupEnv(prefix + key); ok {
                return v
        }
        return def
}

func envFloat(key string, def float64) float64 {
        if v, ok := os.LookupEnv(prefix + key); ok {
                f, err := strconv.ParseFloat(v, 64)
                if err == nil {
                        return f
                }
        }
        return def
}

// порядок в Flags(): сначала env поверх дефолтов, потом флаги поверх env:
c.HTTPAddr = envString("HTTP_ADDR", c.HTTPAddr)
c.SampleRate = envFloat("SAMPLE_RATE", c.SampleRate)
flag.StringVar(&c.HTTPAddr, "http-addr", c.HTTPAddr, "HTTP listen address")
```

Хелперы инкапсулируют всю работу с os.LookupEnv — других обращений к env в проекте быть не должно. Обрати внимание на порядок вызовов: env применяется к уже заполненным дефолтам, и только потом флаги объявляются с env-значением как дефолтом — flag.Parse, вызванный после, перезапишет только явно переданные аргументы. Ошибка парсинга в envFloat честно игнорируется: кривое значение оставить без действия опасно, но validate() ниже всё равно не пропустит невалидные диапазоны; в проде кривое значение надо логировать — добавь лог через переданный logger, если хочешь громче.

### Безопасные дефолты и fail fast

Дефолт — это не «значение на всякий случай», а дизайн-решение: правильный дефолт делает первый запуск успешным, а неправильный — превращает прод в отладку. Проверь себя двумя вопросами: что случится, если запустить сервис с нулём настроек (должен подняться локально и залогировать, куда ходит), и что случится, если продовая переменная опечаталась (должен упасть на старте, а не на сотом запросе). Второй вопрос — принцип fail fast: невалидная конфигурация обнаруживается в validate() до поднятия слушателей, до регистрации в Consul из урока 12 и до первого запроса; ошибка возвращается, main из урока 17 пишет её в лог и завершает процесс с ненулевым кодом выхода. Оркестратор увидит честный крах, перезапустит под и покажет красный статус — вместо зомби, который «работает», но ходит не туда. Что проверять: адреса непустые, SampleRate в диапазоне от 0 до 1 (канон сэмплеров из урока 08), LogLevel из белого списка уровней из урока 06, для лимитера из урока 09 — rate больше нуля, для breaker из урока 10 — таймауты положительные. И у каждой будущей настройки то же правило: когда в уроке 19 появится GracePeriod — время дренажа, он придёт в Config и в validate, а не магической константой в коде. Каждое правило — одна строка и один понятный текст ошибки. Так работает конфигурация микросервисов: невалидно — значит, не стартуем.

### validate(): правила, а не догмы

```go
func (c Config) validate() error {
        switch {
        case c.HTTPAddr == "" || c.GRPCAddr == "":
                return errors.New("config: адреса слушателей обязательны")
        case c.ConsulAddr == "":
                return errors.New("config: адрес Consul обязателен")
        case c.SampleRate < 0 || c.SampleRate > 1:
                return fmt.Errorf("config: sample-rate %v вне [0, 1]", c.SampleRate)
        case c.LogLevel != "info" && c.LogLevel != "debug" && c.LogLevel != "warn":
                return fmt.Errorf("config: неизвестный log-level %q", c.LogLevel)
        }
        return nil
}

// main из урока 17 получает fail fast бесплатно:
func main() {
        if err := run(); err != nil {
                fmt.Fprintln(os.Stderr, err)
                os.Exit(1) // оркестратор видит крах и рестартует под
        }
}
```

Четыре правила покрывают канон курса: слушатели из уроков 04 и 13, Consul из урока 12, сэмплер из урока 08, уровни kitlog-логгера из урока 06. Текст ошибки называет поле и плохое значение — при ночном инциденте это экономит минуты. Важно: validate вызывается ДО сборки зависимостей — неконфигурируемый сервис не должен успеть открыть соединение с базой или зарегистрироваться в реестре. os.Exit(1) — честный сигнал для supervisor'а: exit-код — тоже конфигурационный контракт между сервисом и оркестратором.

### viper и файлы: когда они нужны

Когда полей становится десятки, а сред — много, рождается желание вынести конфигурацию в yaml или toml и менять её без пересборки и перезапуска флагов; стандартный инструмент экосистемы — spf13/viper. Он читает файлы, env с префиксами и дефолты в одном конвейере: viper.SetDefault задаёт безопасный дефолт, viper.SetEnvPrefix и viper.AutomaticEnv подключают env-канал, viper.SetEnvKeyReplacer мапит точки в подчёркивания, viper.ReadInConfig разбирает файл. Честная сторона медали: viper — тяжёлая зависимость для сервиса, у которого четыре поля, и в go-kit конфигурация не вынесена в отдельный модуль именно потому, что конфигурация — не слой сервиса, а обязанность main из урока 17; библиотека, которая диктовала бы способ конфигурирования, нарушала бы собственную философию из [урока 01-filosofiya](/courses/go-kit/lessons/01-filosofiya/). Возьми viper, когда выполняются ОБА условия: полей больше двадцати и есть непрограммист, которому нужен читаемый файл. А вот WatchConfig и hot reload применяй с осторожностью: большинство значений (адреса слушателей, сэмплер) читаются один раз на старте, и живая перезагрузка нужна разве что лимитам — их в курсе умеет пересобирать срез endpoint.Middlewares из [урока 02-endpoint](/courses/go-kit/lessons/02-endpoint/), который «собирается по флагам запуска или конфигу»: пересобери срез — и лимитер из урока 09 начнёт вести себя иначе без рестарта.

### Секреты: не в флагах, не в git, не в логах

Токены, пароли и ключи — особый класс значений, и у него свои запреты. Флаги не годятся физически: их видно в ps aux каждому пользователю машины и в history shell'а. Git не годится по определению: секрет в репозитории leak'ает при первом же форке, и чистка истории не помогает. Env — приемлемый минимум, так передают значения почти все оркестраторы; взрослее — файлы, смонтированные платформой, и secret-менеджеры вроде Vault, из которых main читает значение при старте. Но есть канал утечки, о котором забывают чаще: логи. Логгер из [урока 06-logirovanie](/courses/go-kit/lessons/06-logirovanie/) пишет структурные пары, и один невинный logger.Log("config", cfg) вывалит всё, включая секреты, в централизованный сбор — читатели логов обычно не те же люди, что владельцы секретов. Правило: логируй конфигурацию ПОЛЯМИ, а не структурой целиком, а секретные поля маскируй или пропускай; validate() как раз место, где удобно залогировать собранную конфигурацию без чувствительных значений. И никогда не подставляй секреты в дефолты: дефолт живёт в коде, значит — в git.

**Вопрос:** Стратегия сбора: STRINGSVC_HTTP_ADDR=:9090 задан в окружении, флаг -http-addr не передан, в коде дефолт :8081. На чём поднимется слушатель?

- На :8081 — дефолт всегда сильнее env
- ✅ На :9090 — env перезаписал дефолт, а флаг не задан
- На случайном свободном порту — конфликт источников
- Сервис упадёт: два источника заданы одновременно

Приоритет курса: дефолт → env → флаг. Env-переменная перезаписала дефолт, флаг отсутствует — слушатель встаёт на :9090. Если бы был передан и флаг, он победил бы env, потому что читается последним. Конфликт источников — не ошибка, а осознанный каскад: каждый уровень уточняет предыдущий.

**Вопрос:** Где по канону курса живёт чтение os.LookupEnv?

- В методах сервиса — так значения под рукой
- В endpoint-конструкторах — они первыми получают запрос
- ✅ В пакете config, вызываемом из композиционного корня: больше нигде в проекте
- В transport — декодер знает про окружение запроса

Урок 03 запретил env в глубине методов, урок 17 отвёл сборку композиционному корню, этот урок сводит оба правила: единственное место, где сервис касается окружения, — пакет config, который зовёт main до всякой сборки. Дальше по проекту едут значения: сервис получает их параметрами конструктора, и тест собирается одной строкой без env.

**Вопрос:** Оператор опечатался в продовой переменной STRINGSVC_CONSUL_ADDR. Что сделает правильно настроенный сервис?

- Поднимется на дефолте localhost:8500 и попробует достучаться до дев-агента
- ✅ Упадёт при старте с ошибкой validate: fail fast до слушателей и регистрации
- Поднимется, а первый запрос упадёт с таймаутом
- Зарегистрируется в Consul, а регистрация отвалится потом

Пустой или невалидный адрес ловится в validate() до поднятия слушателей и до registrar.Register() из урока 12 — принцип fail fast. Оркестратор видит ненулевой exit-код, рестартует под и красит статус; инцидент длится секунды и виден сразу. Сервис, поднявшийся «на чём попало», — худший исход: он выглядит живым, отвечает health check'ам и портит данные в чужой среде.

### sample-rate вне диапазона

```go
$ go run ./cmd/stringsvc -sample-rate=1.5
// Flags(): env нет, флаг прочитан, SampleRate = 1.5
// дальше вызывается c.validate()
```

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

- Сервис поднимется и будет трассировать каждый запрос — сэмплер сам поймёт
- Сервис поднимется, а Jaeger молча выбросит невалидные спаны
- ✅ Сервис упадёт на старте: validate вернёт ошибку, main выйдет с кодом 1, слушатели :8081 и :8082 не поднимались
- Сервис поднимется на дефолтном 0.01: невалидный флаг игнорируется

Диапазон сэмплинга из урока 08 — от 0 до 1; 1.5 не имеет смысла ни для const, ни для probabilistic-сэмплера. validate() возвращает ошибку с именем поля и значением, run() её пробрасывает, main пишет в stderr и завершает процесс с os.Exit(1) — ДО net.Listen и до registrar.Register(). Это и есть fail fast: ошибка конфигурации стоит секунды на старте, а не часы расследования «почему трейсы выглядят странно».

### Пустая переменная против отсутствующей

```go
// На машине экспортирована пустая переменная:
export STRINGSVC_SAMPLE_RATE=
// дефолт: 0.01

// envFloat читает LookupEnv и видит ok=true, ParseFloat("") даёт ошибку
// envString на пустой переменной вернёт "" — и это валидное переопределение
```

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

- Ничего не изменится: пустая переменная равна отсутствующей
- ✅ SampleRate останется дефолтным 0.01 — ParseFloat упал, хелпер вернул def
- HTTP_ADDR останется пустым: envString честно вернул пустую строку, validate поймает пустой адрес
- Сервис упадёт на ParseFloat в envFloat

Пустая переменная и отсутствующая — разные события, и LookupEnv их различает. Для числа пустая строка не парсится — envFloat возвращает дефолт, и SampleRate остаётся 0.01: кривое значение не пролезает в конфиг. Для строки пустое значение — легальное переопределение: envString вернёт "", HTTPAddr станет пустым, и validate() честно упадёт с «адрес обязателен». Разница между «упало на старте» и «упало молча» — в том, что validate зовётся всегда.

### Собери конфигурацию из трёх источников

Восстанови четыре строки. Пропуски — по порядку: функция чтения env, различающая «пусто» и «нет», возвращаемое значение при отсутствии переменной, конструктор флага для строки, метод валидации перед выдачей Config.

```go
func envString(key, def string) string {
        if v, ok := os.___(prefix + key); ok {
                return v
        }
        return ___
}

func Flags() (Config, error) {
        var c Config
        c.HTTPAddr = envString("HTTP_ADDR", ":8081")
        flag.___(&c.HTTPAddr, "http-addr", c.HTTPAddr, "HTTP listen address")
        flag.Parse()
        if err := c.___(); err != nil {
                return Config{}, err
        }
        return c, nil
}
```

Подсказка: Первый пропуск — функция пакета os, возвращающая значение и флаг существования (не Getenv); второй — параметр-дефолт; третий — String-вариант регистратора флага; четвёртый — метод Config из этого урока, который ловит невалидные значения до слушателей.

Правильные ответы в пропусках: `LookupEnv`, `def`, `StringVar`, `validate`

### Путь конфигурации при старте сервиса

Расставь шаги старта по порядку: от процесса до слушателей.

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

1. `Процесс стартовал: run() вызван из main`
2. `flag.Parse разобрал аргументы командной строки поверх env-значений`
3. `env-хелперы прочитали STRINGSVC_-переменные поверх дефолтов через os.LookupEnv`
4. `validate() проверил диапазоны, адреса и белые списки`
5. `Невалидно: ошибка уходит в лог, процесс выходит с кодом 1 — fail fast`
6. `Валидно: из Config построены logger урока 06 и tracer урока 08`
7. `Репозиторий и consul-регистратор получили адреса из Config`
8. `Слушатели встали на HTTPAddr и GRPCAddr, регистрация в Consul с адресом из Config`

### Источники конфигурации

| Источник | Инструмент | Когда оправдан | Главный риск |
| --- | --- | --- | --- |
| Дефолты в коде | литералы в Flags() | локальный запуск и дев без настроек | прод остался на дефолтах — лови валидацией |
| env-переменные | os.LookupEnv + префикс | контейнеры, Kubernetes, CI | строковые значения — парсить и валидировать |
| Флаги | flag | локальный запуск, docker run, отладка | видны в ps — секретам здесь не место |
| Файл yaml/toml | viper | десятки полей и непрограммист за настройками | секрет в git — репозиторий leak'ает |
| Secret store | Vault, secrets оркестратора | токены, пароли, ключи | сложность и доступы — начинать не с него |
| Hot reload | viper WatchConfig | лимиты и доли сэмплинга на живом трафике | состояние меняется на лету без рестарта |

> **Три дыры, через которые уходят секреты.** Флаг -db-password живёт в ps aux и в shell history: любой локальный пользователь прочтёт пароль. Секрет в git чистится плохо: форк, кэш CI, зеркала — история уже разошлась. Лог с полной структурой Config уезжает в централизованный сбор, где читатели не совпадают с владельцами секрета. Рабочий минимум: секреты только через env или secret store, в логах — маскированные поля, в дефолтах — никогда. Прогони grep по репозиторию на парольные слова перед первым продом — дешевле, чем ротация всех ключей.

> **Передавай значения, а не *Config.** Соблазн — прокинуть *Config во все конструкторы: service.New(cfg), endpoint.New(cfg, ...). Не поддавайся: сервис, принимающий *Config, снова знает обо всём окружении, и граница слоёв из урока 17 размывается — тесту уже нужен целый Config, чтобы собрать один метод. Канон курса: конструкторы принимают конкретные значения и зависимости (addr string, rate float64, repo Repository), а Config живёт только в main и пакете config. Тогда тест собирается заглушками, а среда не протекает в бизнес-логику.

> **Честно о запуске кода.** Урок про конфигурацию проверяется запуском локально: в песочнице курса сторонние модули и процессы не исполняются, поэтому интерактивы построены на чтении кода. Локально: запусти сервис без флагов и убедись, что дефолты поднимают оба слушателя; затем задай STRINGSVC_HTTP_ADDR=:9090 и флаг -sample-rate=1.5 — первый должен переехать, второй — уронить процесс с осмысленной ошибкой до поднятия слушателей.

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

- **Twelve-Factor III** — Конфигурация хранится в окружении, а не в коде: один артефакт живёт во всех средах, меняется только env — пункт свода правил контейнерных платформ.
- **os.LookupEnv** — Чтение переменной с различением «пусто» и «нет»: возвращает значение и ok; в отличие от Getenv не путает пустое переопределение с отсутствием.
- **Fail fast** — Невалидная конфигурация ловится validate() до слушателей и регистрации; процесс выходит с кодом 1, оркестратор рестартует под с честным статусом.
- **Безопасный дефолт** — Значение флага, при котором сервис работает локально без настроек; прод всегда переопределяет через env — дефолт никуда не секретит.
- **viper** — Библиотека конфигурации: файлы yaml/toml, env с префиксами (AutomaticEnv), дефолты; оправдана при десятках полей, для четырёх флагов избыточна.
- **Приоритет источников** — Дефолт → env → флаг: что задано явнее, то побеждает; флаг читается последним и переопределяет всё остальное.

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

Сервис научился рождаться в любой среде: Config собран из дефолтов, env и флагов, validate отсеивает кривые значения, main из урока 17 кормит конструкторы готовыми числами и строками. Но рождение — половина жизненного цикла; вторая половина — смерть, и по умолчанию она некрасивая: Ctrl+C и SIGTERM от оркестратора убивают процесс мгновенно, недоделанные запросы обрываются, а консул-запись из урока 12 остаётся зомби в реестре. Урок 19-graceful исправит это: сигналы, дренаж HTTP- и gRPC-вызовов, вычеркивание из ротации до смерти процесса и закрытие трейсера. Детали настройки наблюдаемости из конфигурации освежи в [уроке 08-tracing](/courses/go-kit/lessons/08-tracing/) и [уроке 06-logirovanie](/courses/go-kit/lessons/06-logirovanie/), карту всех слоёв — на [странице курса](/courses/go-kit/); а если хочется освежить, зачем env запрещён в методах сервиса, вернись к [уроку 03-service](/courses/go-kit/lessons/03-service/): там у этого запрета есть причина, а не только правило.

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

- Конфигурация микросервисов — значения среды, а не код: Twelve-Factor III, один артефакт живёт во всех средах без пересборки.
- Три источника с приоритетом: дефолты в коде, env-переменные, флаги; что задано явнее — то побеждает.
- Вся конфигурация — одна структура Config и один конструктор Flags() в пакете config; сервис и endpoint'ы env не видят никогда.
- os.LookupEnv вместо os.Getenv: различение «пусто» и «нет» спасает от тихих дефолтов; префикс STRINGSVC_ разводит сервисы.
- Fail fast: validate() ловит невалидные значения до слушателей и регистрации в Consul, процесс выходит с кодом 1.
- viper оправдан при десятках полей и непрограммисте за настройками; hot reload нужен редко — лимиты умеет пересобирать срез endpoint.Middlewares.
- Секреты не в флагах (ps), не в git, не в логах: env или secret store, поля логируются маскированно.
- Конструкторы принимают значения, а не *Config: иначе среда протекает в сервис и тест перестаёт собираться одной строкой.

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

Настрой конфигурацию stringsvc по уроку. Первое: собери пакет internal/config со структурой Config — порты HTTP и gRPC из уроков 04 и 13, адрес Consul из урока 12, сэмплер из урока 08, уровень логов из урока 06 — и конструктором Flags(), где каждый флаг имеет безопасный дефолт. Второе: добавь env-хелперы на os.LookupEnv с префиксом STRINGSVC_ и проверь каскад приоритетов: запусти без настроек (дефолты), с env STRINGSVC_HTTP_ADDR=:9090 (env победил), с env и флагом -http-addr=:9091 (флаг победил) — запиши три адреса слушателей в конспект. Третье: напиши validate() с четырьмя правилами урока и урони сервис флагом -sample-rate=2 — убедись, что exit-код ненулевой, в stderr текст с именем поля, а слушатели не поднимались. Четвёртое: прогони grep по репозиторию на парольные слова и убедись, что ни один секрет не живёт в коде или дефолтах; логируй собранную конфигурацию полями без чувствительных значений. Пятое: проверь канон урока 03 — сервис и endpoint'ы не импортируют пакет os и config; если импортируют, перенеси значения в конструкторы и добейся, чтобы go test ./... собирался в чистой среде без единой переменной.

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

### Что сильнее — env или флаг? В интернетах спорят.

Это не свойство среды, а решение приложения: важен порядок чтения в твоём Flags(). В схеме курса дефолт читается первым, env перезаписывает его, флаг объявляется с env-значением как дефолтом и применяется последним — значит, флаг сильнее. Контейнеры обычно вообще не передают флаги, поэтому в проде решает env, а флаги остаются инструментом локального запуска и отладки. Зафиксируй порядок в коде и не меняй его между сервисами.

### Нужен ли viper маленькому сервису?

Скорее нет: для четырёх-десяти полей хватает flag и os.LookupEnv без единой сторонней зависимости. viper оправдан, когда полей десятки, конфигурацию ведёт непрограммист или нужны файлы разных форматов и hot reload. При этом горячая перезагрузка нужна меньшинству значений: адреса и сэмплер читаются один раз, а лимиты умеет пересобирать срез endpoint.Middlewares из урока 02 — для него конфиг и пишут.

### Как не засветить секреты в логах при отладке конфигурации?

Логируй конфигурацию полями, а не структурой целиком: пары вида addr и sample-rate полезны, db-password — нет. Сделай метод, возвращающий версию Config с маскированными чувствительными полями, и логируй её из validate() при старте — увидишь всё, что нужно для расследования, и не сольёшь ничего. Секреты при этом живут в env или secret store и никогда не попадают в дефолты, код и флаги.
