Опирается на правила:
R-VLD-CFG-1,R-VLD-CFG-2,R-VLD-CFG-4,R-VLD-CFG-X1,R-VLD-CFG-X2,R-VLD-WHERE-2из Validation Style Guide → раздел 7. Конфигурация.
Важно знать
config.Load()— две фазы:envconfig.Process(чтение переменных окружения → struct) +validate.Struct(проверка ограничений). Ошибка в любой фазе →os.Exit(1).- Обязательное поле —
required:"true"в envconfig-теге. Пустая переменная окружения приrequired:"true"даёт ошибку на старте, не в рантайме.- Дополнительные ограничения — validate-теги:
validate:"url",validate:"hostname_port",validate:"min=1,max=120". Без них envconfig проверяет только тип и presence.- Nested-struct:
envconfig.Processраскрывает вложенные поля по префиксу (или черезenvconfig:","встраивание);validate.Structпо умолчанию не входит в nested — нуженvalidate.Varили отдельныйvalidate.Structдля каждой вложенной структуры.- Optional-поля — без
required:"true", cdefault:"..."или без него. Pointer-тип (*string) сигнализирует «может быть nil».- Деньги и целочисленные лимиты —
int/int64, неfloat64; тегvalidate:"min=1"вместоrequiredдля числа, где ноль недопустим.os.Getenv("KEY")для required-конфига — антипаттерн: нет типизации, нет валидации, нет default.
Конфиг — единственное место, где «упасть на старте» лучше, чем «работать с битыми значениями». Неправильный PAYMENT_BASE_URL в DTO даёт 400 одному клиенту; тот же URL в конфиге сервиса ломает все платежи до перезапуска. fail-fast через envconfig + validate.Struct ловит проблему до того, как поступил первый запрос.
Двухфазный Load
R-VLD-CFG-1 / R-VLD-WHERE-2: конфиг валидируется на старте, результат — полностью готовая и проверенная struct.
// config/config.go
package config
import (
"fmt"
"time"
"github.com/go-playground/validator/v10"
"github.com/kelseyhightower/envconfig"
)
var validate = validator.New()
type Config struct {
DatabaseURL string `envconfig:"DATABASE_URL" required:"true" validate:"url"`
RedisAddr string `envconfig:"REDIS_ADDR" required:"true" validate:"hostname_port"`
HTTPTimeout time.Duration `envconfig:"HTTP_TIMEOUT" default:"5s"`
Payment PaymentConfig
}
type PaymentConfig struct {
BaseURL string `envconfig:"PAYMENT_BASE_URL" required:"true" validate:"url"`
APIKey string `envconfig:"PAYMENT_API_KEY" required:"true" validate:"min=32"`
TimeoutSec int `envconfig:"PAYMENT_TIMEOUT" default:"10" validate:"min=1,max=120"`
}
func Load() (*Config, error) {
var cfg Config
if err := envconfig.Process("APP", &cfg); err != nil {
return nil, fmt.Errorf("config: %w", err)
}
if err := validate.Struct(&cfg); err != nil {
return nil, fmt.Errorf("config validate: %w", err)
}
if err := validate.Struct(&cfg.Payment); err != nil {
return nil, fmt.Errorf("config validate payment: %w", err)
}
return &cfg, nil
}
main.go вызывает Load() и при ошибке завершается немедленно:
// cmd/order-service/main.go
func main() {
cfg, err := config.Load()
if err != nil {
slog.Error("недопустимая конфигурация", "error", err)
os.Exit(1)
}
// дальше — wire зависимостей, запуск HTTP-сервера
run(cfg)
}
Что происходит при отсутствующем APP_DATABASE_URL:
envconfig.Processвозвращает ошибку:required key APP_DATABASE_URL missing value.slog.Errorпишет причину в лог.os.Exit(1)— процесс завершается. Kubernetes/systemd перезапустит и увидит тот же exit code. Healthcheck красный, деплой откатится.
Без валидации конфиг поднимается с пустым DatabaseURL, первый запрос к БД падает с pq: invalid connection string, причина — в пяти строках стека, а не в конфиге.
Обязательные и опциональные поля
R-VLD-CFG-2: обязательное поле — required:"true" без default. Опциональное — без required, можно с default.
// config/sber_client.go
type SberClientConfig struct {
BaseURL string `envconfig:"SBER_BASE_URL" required:"true" validate:"url"`
APIKey string `envconfig:"SBER_API_KEY" required:"true" validate:"min=16"`
Timeout time.Duration `envconfig:"SBER_TIMEOUT" default:"10s"`
MaxRetries int `envconfig:"SBER_MAX_RETRIES" default:"3" validate:"min=1,max=10"`
ProxyURL string `envconfig:"SBER_PROXY_URL"` // optional
}
Для числового поля, где ноль недопустим, используй validate:"min=1", а не required:
type ProductConfig struct {
MaxStockQty int `envconfig:"MAX_STOCK_QTY" default:"10000" validate:"min=1"`
ReorderPoint int `envconfig:"REORDER_POINT" default:"100" validate:"min=0"`
PriceFloorCop int64 `envconfig:"PRICE_FLOOR_COP" required:"true" validate:"gt=0"`
}
validate:"required" на int в validator/v10 считает 0 невалидным — тип примитива не различает «не задан» и «явный ноль». Для числа без нуля — validate:"min=1" или validate:"gt=0".
Validate-теги для дополнительных ограничений
envconfig проверяет присутствие и умеет парсить типы (time.Duration, bool). Для дополнительных правил — validate-теги:
type CustomerServiceConfig struct {
GRPCURL string `envconfig:"CUSTOMER_GRPC_URL" required:"true" validate:"hostname_port"`
TLSCertPath string `envconfig:"CUSTOMER_TLS_CERT" required:"true"`
PoolSize int `envconfig:"CUSTOMER_POOL_SIZE" default:"10" validate:"min=1,max=50"`
CallTimeout time.Duration `envconfig:"CUSTOMER_TIMEOUT" default:"5s"`
RetryCount int `envconfig:"CUSTOMER_RETRIES" default:"2" validate:"min=0,max=5"`
}
validate:"hostname_port" — строка вида host:port; validate:"url" — полный URL с схемой; validate:"min=32" на string — длина не менее 32 символов (подходит для ключей и токенов).
time.Duration парсится envconfig-ом: 5s, 500ms, 1m30s. Ошибка формата — envconfig.Process вернёт ошибку. Дополнительных validate-тегов для Duration нет — при необходимости напишите кастомный тег (см. go/custom-constraints.md).
Nested-конфиг
R-VLD-CFG-4: вложенные структуры валидируются явно — validate.Struct не входит в nested автоматически (в отличие от Jakarta @Valid).
// config/config.go
type Config struct {
HTTP HTTPConfig
Database DatabaseConfig
Kafka KafkaConfig
}
type HTTPConfig struct {
Port int `envconfig:"HTTP_PORT" default:"8080" validate:"min=1,max=65535"`
ReadTimeout time.Duration `envconfig:"HTTP_READ_TIMEOUT" default:"30s"`
WriteTimeout time.Duration `envconfig:"HTTP_WRITE_TIMEOUT" default:"30s"`
IdleTimeout time.Duration `envconfig:"HTTP_IDLE_TIMEOUT" default:"120s"`
}
type DatabaseConfig struct {
URL string `envconfig:"DATABASE_URL" required:"true" validate:"url"`
MaxOpenConn int `envconfig:"DATABASE_MAX_OPEN_CONN" default:"25" validate:"min=1,max=200"`
MaxIdleConn int `envconfig:"DATABASE_MAX_IDLE_CONN" default:"5" validate:"min=0,max=200"`
}
type KafkaConfig struct {
Brokers string `envconfig:"KAFKA_BROKERS" required:"true"`
ConsumerGroup string `envconfig:"KAFKA_CONSUMER_GROUP" required:"true" validate:"min=1"`
SessionTimeout int `envconfig:"KAFKA_SESSION_TIMEOUT" default:"30" validate:"min=6,max=300"`
}
func Load() (*Config, error) {
var cfg Config
if err := envconfig.Process("APP", &cfg); err != nil {
return nil, fmt.Errorf("config: %w", err)
}
for _, s := range []any{&cfg.HTTP, &cfg.Database, &cfg.Kafka} {
if err := validate.Struct(s); err != nil {
return nil, fmt.Errorf("config validate: %w", err)
}
}
return &cfg, nil
}
Цикл по []any позволяет масштабировать: добавление нового nested-конфига требует только добавить его в срез.
Альтернатива — вспомогательная функция:
func mustValidate(v *validator.Validate, sections ...any) error {
for _, s := range sections {
if err := v.Struct(s); err != nil {
return err
}
}
return nil
}
os.Getenv для required-конфига — антипаттерн
R-VLD-CFG-X2: os.Getenv и os.LookupEnv не дают ни типизации, ни валидации, ни default.
// НЕЛЬЗЯ
func newSberClient() *SberClient {
baseURL := os.Getenv("SBER_BASE_URL") // пустая строка при отсутствии
apiKey := os.Getenv("SBER_API_KEY")
return &SberClient{baseURL: baseURL, apiKey: apiKey}
}
Что не так:
os.Getenvвозвращает пустую строку, а не ошибку, если переменная не задана.- Нет
required— сервис стартует с пустымbaseURL. - Нет типизации —
timeout := os.Getenv("SBER_TIMEOUT")даст строку, а неtime.Duration. - Конфиг размазан по кодовой базе: каждый пакет читает свои переменные, нет единой точки входа.
Правильно: всё через config.Load(), вызывается один раз в main.go, результат передаётся как зависимость:
// ХОРОШО
type SberClient struct {
cfg SberClientConfig
}
func NewSberClient(cfg SberClientConfig) *SberClient {
return &SberClient{cfg: cfg}
}
// main.go
cfg, err := config.Load()
// ...
sberClient := sber.NewSberClient(cfg.Sber)
Конфиг без validate-тегов
R-VLD-CFG-X1: struct без validate-тегов на required-полях полагается только на envconfig — присутствие поля проверяется, содержимое — нет.
// ПЛОХО — нет validate-тегов
type OrderServiceConfig struct {
DatabaseURL string `envconfig:"DATABASE_URL" required:"true"` // пустая строка пройдёт, если так задана переменная
HTTPPort int `envconfig:"HTTP_PORT" default:"8080"` // 0 и 99999 не будут пойманы
}
// ХОРОШО
type OrderServiceConfig struct {
DatabaseURL string `envconfig:"DATABASE_URL" required:"true" validate:"url"`
HTTPPort int `envconfig:"HTTP_PORT" default:"8080" validate:"min=1,max=65535"`
}
required:"true" гарантирует, что переменная присутствует в окружении. Но DATABASE_URL=" " (пробел) пройдёт envconfig. validate:"url" отловит невалидную строку на этапе validate.Struct.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
| Конфиг-struct без validate-тегов на ограничениях | R-VLD-CFG-X1 | validate:"url", validate:"min=1" и другие теги |
os.Getenv("KEY") для required-конфига | R-VLD-CFG-X2 | envconfig.Process + required:"true" |
validate.Struct только на корневом конфиге, nested без вызова | R-VLD-CFG-4 | validate.Struct(&cfg.Payment), validate.Struct(&cfg.Kafka) и т.д. |
validate:"required" на int для проверки «не ноль» | R-VLD-STD-X1 | validate:"min=1" или validate:"gt=0" |
Деньги в float64 с validate:"gt=0.0" | R-VLD-STD-5 | int64 (копейки) + validate:"gt=0" |
Конфиг читается пакетами напрямую через os.Getenv | R-VLD-CFG-X2 | единый config.Load() в main.go, struct передаётся как зависимость |
Куда дальше
- go/where-to-validate.md — где валидировать: HTTP-граница, конфиг, домен.
- go/standard-constraints.md — какие теги validator/v10 использовать на каких типах.
- go/custom-constraints.md — кастомные теги:
future,ru_phone,vat_number. - go/cross-field-validation.md —
RegisterStructValidationдля правил с несколькими полями. - go/messages-and-i18n.md — русские тексты ошибок через
Localize(fe). - Resilience Style Guide — таймауты в конфиге клиентов (
R-RES-*).