@nestjs/config → caarlos0/env + os.Getenv
dotenv → godotenv
config.yaml (js-yaml) → gopkg.in/yaml.v3
@Injectable() + DI-контейнер → ручная DI через конструкторы
Provider в NestJS → функция-конструктор в Go
ConfigService → структура Config + провайдер
caarlos0/env
gopkg.in/yaml.v3mkdir go-di-config && cd go-di-config
go mod init go-di-config
# Устанавливаем зависимости
go get github.com/caarlos0/env/v11
go get gopkg.in/yaml.v3
go get github.com/joho/godotenv
go get github.com/redis/go-redis/v9
go get github.com/jackc/pgx/v5/pgxpool
go mod tidy
# Создаём структуру
mkdir -p internal/config
mkdir -p internal/provider
mkdir -p cmd/server
config.yaml# Конфигурация по умолчанию (development)
server:
port: 8080
host: "0.0.0.0"
read_timeout: 5s
write_timeout: 10s
idle_timeout: 120s
database:
host: "localhost"
port: 5432
user: "postgres"
password: "secret"
dbname: "myapp"
sslmode: "disable"
pool_max_conns: 20
pool_min_conns: 5
redis:
addr: "localhost:6379"
password: ""
db: 0
pool_size: 20
logging:
level: "debug" # debug, info, warn, error
format: "json" # json, text
output: "stdout" # stdout, file
auth:
jwt_secret: "change-me-in-production"
jwt_ttl: 24h
bcrypt_cost: 12
# Функциональные флаги
features:
enable_cache: true
enable_metrics: true
enable_tracing: false
.env (пример)# Переменные окружения переопределяют config.yaml
APP_ENV=development
DATABASE_URL=postgres://postgres:secret@localhost:5432/myapp?sslmode=disable
REDIS_URL=redis://localhost:6379/0
AUTH_JWT_SECRET=super-secret-key-here
internal/config/config.gopackage config
import (
"fmt"
"os"
"time"
"github.com/caarlos0/env/v11"
"gopkg.in/yaml.v3"
)
// Config — корневая структура конфигурации
type Config struct {
AppEnv string `yaml:"app_env" env:"APP_ENV" envDefault:"development"`
Server ServerConfig `yaml:"server"`
Database DBConfig `yaml:"database"`
Redis RedisConfig `yaml:"redis"`
Logging LogConfig `yaml:"logging"`
Auth AuthConfig `yaml:"auth"`
Features Features `yaml:"features"`
}
type ServerConfig struct {
Host string `yaml:"host" env:"SERVER_HOST" envDefault:"0.0.0.0"`
Port int `yaml:"port" env:"SERVER_PORT" envDefault:"8080"`
ReadTimeout time.Duration `yaml:"read_timeout" env:"SERVER_READ_TIMEOUT" envDefault:"5s"`
WriteTimeout time.Duration `yaml:"write_timeout" env:"SERVER_WRITE_TIMEOUT" envDefault:"10s"`
IdleTimeout time.Duration `yaml:"idle_timeout" env:"SERVER_IDLE_TIMEOUT" envDefault:"120s"`
}
// Addr — адрес для HTTP-сервера
func (s ServerConfig) Addr() string {
return fmt.Sprintf("%s:%d", s.Host, s.Port)
}
type DBConfig struct {
Host string `yaml:"host" env:"DATABASE_HOST"`
Port int `yaml:"port" env:"DATABASE_PORT" envDefault:"5432"`
User string `yaml:"user" env:"DATABASE_USER"`
Password string `yaml:"password" env:"DATABASE_PASSWORD"`
DBName string `yaml:"dbname" env:"DATABASE_NAME"`
SSLMode string `yaml:"sslmode" env:"DATABASE_SSLMODE" envDefault:"disable"`
PoolMaxConn int `yaml:"pool_max_conns" env:"DATABASE_POOL_MAX" envDefault:"20"`
PoolMinConn int `yaml:"pool_min_conns" env:"DATABASE_POOL_MIN" envDefault:"5"`
// URL — полный DSN (имеет приоритет)
URL string `yaml:"url" env:"DATABASE_URL"`
}
// DSN — формирует строку подключения
func (d DBConfig) DSN() string {
if d.URL != "" {
return d.URL
}
return fmt.Sprintf(
"postgres://%s:%s@%s:%d/%s?sslmode=%s",
d.User, d.Password, d.Host, d.Port, d.DBName, d.SSLMode,
)
}
type RedisConfig struct {
Addr string `yaml:"addr" env:"REDIS_ADDR" envDefault:"localhost:6379"`
Password string `yaml:"password" env:"REDIS_PASSWORD"`
DB int `yaml:"db" env:"REDIS_DB" envDefault:"0"`
PoolSize int `yaml:"pool_size" env:"REDIS_POOL_SIZE" envDefault:"20"`
// URL — полный URL (имеет приоритет)
URL string `yaml:"url" env:"REDIS_URL"`
}
type LogConfig struct {
Level string `yaml:"level" env:"LOG_LEVEL" envDefault:"info"`
Format string `yaml:"format" env:"LOG_FORMAT" envDefault:"json"`
Output string `yaml:"output" env:"LOG_OUTPUT" envDefault:"stdout"`
}
type AuthConfig struct {
JWTSecret string `yaml:"jwt_secret" env:"AUTH_JWT_SECRET"`
JWTTTL time.Duration `yaml:"jwt_ttl" env:"AUTH_JWT_TTL" envDefault:"24h"`
BcryptCost int `yaml:"bcrypt_cost" env:"AUTH_BCRYPT_COST" envDefault:"12"`
}
type Features struct {
EnableCache bool `yaml:"enable_cache" env:"FEATURE_CACHE" envDefault:"true"`
EnableMetrics bool `yaml:"enable_metrics" env:"FEATURE_METRICS" envDefault:"true"`
EnableTracing bool `yaml:"enable_tracing" env:"FEATURE_TRACING" envDefault:"false"`
}
// ╔══════════════════════════════════════════════════════════╗
// ║ ЗАГРУЗКА КОНФИГУРАЦИИ ║
// ╚══════════════════════════════════════════════════════════╝
// Load — загружает конфигурацию (12-factor: env > yaml > defaults)
func Load(configPath string) (*Config, error) {
cfg := &Config{}
// 1. Загружаем значения по умолчанию из YAML
if configPath != "" {
if err := loadYAML(configPath, cfg); err != nil {
return nil, fmt.Errorf("load yaml: %w", err)
}
}
// 2. Переопределяем из переменных окружения
if err := env.Parse(cfg); err != nil {
return nil, fmt.Errorf("parse env: %w", err)
}
// 3. Валидация
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("validate config: %w", err)
}
return cfg, nil
}
// MustLoad — загружает конфиг или паникует (для использования в main)
func MustLoad(configPath string) *Config {
cfg, err := Load(configPath)
if err != nil {
panic(fmt.Sprintf("config: %v", err))
}
return cfg
}
// loadYAML — читает YAML-файл
func loadYAML(path string, cfg *Config) error {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil // Файл не обязателен
}
return err
}
return yaml.Unmarshal(data, cfg)
}
// Validate — проверяет обязательные поля
func (c *Config) Validate() error {
if c.Auth.JWTSecret == "" || c.Auth.JWTSecret == "change-me-in-production" {
return fmt.Errorf("AUTH_JWT_SECRET must be set and not default")
}
if c.Database.URL == "" && c.Database.Host == "" {
return fmt.Errorf("database host or DATABASE_URL must be set")
}
if c.Server.Port < 1 || c.Server.Port > 65535 {
return fmt.Errorf("invalid server port: %d", c.Server.Port)
}
if c.Auth.BcryptCost < 4 || c.Auth.BcryptCost > 31 {
return fmt.Errorf("bcrypt cost must be between 4 and 31")
}
return nil
}
// IsDevelopment — проверяет окружение
func (c *Config) IsDevelopment() bool {
return c.AppEnv == "development"
}
// IsProduction — проверяет окружение
func (c *Config) IsProduction() bool {
return c.AppEnv == "production"
}
internal/provider/provider.gopackage provider
import (
"context"
"fmt"
"log"
"time"
"github.com/redis/go-redis/v9"
"github.com/jackc/pgx/v5/pgxpool"
"go-di-config/internal/config"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ ПРОВАЙДЕРЫ — ленивая инициализация зависимостей ║
// ╚══════════════════════════════════════════════════════════╝
// Provider — контейнер зависимостей (DI-контейнер)
type Provider struct {
cfg *config.Config
dbPool *pgxpool.Pool
redisClient *redis.Client
}
// NewProvider — создаёт провайдер (без инициализации!)
func NewProvider(cfg *config.Config) *Provider {
return &Provider{cfg: cfg}
}
// Config — возвращает конфигурацию
func (p *Provider) Config() *config.Config {
return p.cfg
}
// DBPool — лениво инициализирует пул PostgreSQL
func (p *Provider) DBPool(ctx context.Context) (*pgxpool.Pool, error) {
if p.dbPool != nil {
return p.dbPool, nil
}
poolCfg, err := pgxpool.ParseConfig(p.cfg.Database.DSN())
if err != nil {
return nil, fmt.Errorf("parse pg config: %w", err)
}
poolCfg.MaxConns = int32(p.cfg.Database.PoolMaxConn)
poolCfg.MinConns = int32(p.cfg.Database.PoolMinConn)
pool, err := pgxpool.NewWithConfig(ctx, poolCfg)
if err != nil {
return nil, fmt.Errorf("create pg pool: %w", err)
}
// Проверяем соединение
if err := pool.Ping(ctx); err != nil {
return nil, fmt.Errorf("ping pg: %w", err)
}
log.Println("✅ PostgreSQL подключён")
p.dbPool = pool
return pool, nil
}
// RedisClient — лениво инициализирует Redis-клиент
func (p *Provider) RedisClient() (*redis.Client, error) {
if p.redisClient != nil {
return p.redisClient, nil
}
opts := &redis.Options{
Addr: p.cfg.Redis.Addr,
Password: p.cfg.Redis.Password,
DB: p.cfg.Redis.DB,
PoolSize: p.cfg.Redis.PoolSize,
DialTimeout: 5 * time.Second,
ReadTimeout: 3 * time.Second,
WriteTimeout: 3 * time.Second,
MaxRetries: 3,
}
client := redis.NewClient(opts)
// Проверяем соединение
if err := client.Ping(context.Background()).Err(); err != nil {
return nil, fmt.Errorf("redis ping: %w", err)
}
log.Println("✅ Redis подключён")
p.redisClient = client
return client, nil
}
// Close — освобождает все ресурсы
func (p *Provider) Close() {
if p.dbPool != nil {
p.dbPool.Close()
log.Println("PostgreSQL закрыт")
}
if p.redisClient != nil {
p.redisClient.Close()
log.Println("Redis закрыт")
}
}
cmd/server/main.gopackage main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"github.com/joho/godotenv"
"go-di-config/internal/config"
"go-di-config/internal/provider"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("⚙️ Загрузка конфигурации и DI...")
// 1. Загружаем .env (для локальной разработки)
if err := godotenv.Load(); err != nil {
log.Println(" .env не найден (используем системные переменные)")
}
// 2. Загружаем конфигурацию
cfg := config.MustLoad("config.yaml")
// 3. Выводим сводку
log.Printf(" Окружение: %s", cfg.AppEnv)
log.Printf(" Сервер: %s", cfg.Server.Addr())
log.Printf(" БД: %s (max_conns=%d)", cfg.Database.DSN(), cfg.Database.PoolMaxConn)
log.Printf(" Redis: %s (pool_size=%d)", cfg.Redis.Addr, cfg.Redis.PoolSize)
log.Printf(" Логи: level=%s format=%s", cfg.Logging.Level, cfg.Logging.Format)
log.Printf(" Фичи: cache=%v metrics=%v tracing=%v",
cfg.Features.EnableCache,
cfg.Features.EnableMetrics,
cfg.Features.EnableTracing,
)
// 4. Создаём провайдер
prov := provider.NewProvider(cfg)
defer prov.Close()
// 5. Инициализируем зависимости (лениво)
ctx := context.Background()
if cfg.Features.EnableCache {
if _, err := prov.RedisClient(); err != nil {
log.Printf(" ⚠️ Redis недоступен: %v (продолжаем без кэша)", err)
}
}
dbPool, err := prov.DBPool(ctx)
if err != nil {
log.Fatalf(" ❌ БД недоступна: %v", err)
}
_ = dbPool
// 6. Запускаем сервер (в реальности — HTTP/gRPC)
log.Println("✅ Сервер готов к запуску")
log.Println(" (в реальном проекте здесь был бы server.ListenAndServe())")
// 7. Graceful shutdown
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("🛑 Завершение работы...")
}
// Паттерн Options — альтернативный способ конфигурации.
// Позволяет создавать объекты с опциональными параметрами.
package config
// ServerOption — функция, модифицирующая ServerConfig
type ServerOption func(*ServerConfig)
// WithPort — опция: установить порт
func WithPort(port int) ServerOption {
return func(s *ServerConfig) {
s.Port = port
}
}
// WithTimeouts — опция: установить таймауты
func WithTimeouts(read, write time.Duration) ServerOption {
return func(s *ServerConfig) {
s.ReadTimeout = read
s.WriteTimeout = write
}
}
// NewServerConfig — конструктор с опциями
func NewServerConfig(opts ...ServerOption) *ServerConfig {
// Значения по умолчанию
s := &ServerConfig{
Host: "0.0.0.0",
Port: 8080,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Применяем опции
for _, opt := range opts {
opt(s)
}
return s
}
// Пример использования:
// srv := NewServerConfig(
// WithPort(9090),
// WithTimeouts(10*time.Second, 30*time.Second),
// )
# Создаём .env для локальной разработки
cat > .env << 'EOF'
APP_ENV=development
AUTH_JWT_SECRET=dev-secret-key-12345
DATABASE_URL=postgres://postgres:secret@localhost:5432/myapp?sslmode=disable
EOF
# Запускаем
go run ./cmd/server/main.go
# Переопределение через env (12-factor)
APP_ENV=production \
SERVER_PORT=9090 \
AUTH_JWT_SECRET=prod-secret \
go run ./cmd/server/main.go
# Без YAML-файла (только env)
rm config.yaml
SERVER_PORT=3000 \
DATABASE_URL=postgres://user:pass@host/db \
AUTH_JWT_SECRET=supersecret \
go run ./cmd/server/main.go
| Приоритет | Источник | Пример |
|---|---|---|
| 1 (высший) | Переменные окружения | export SERVER_PORT=9090 |
| 2 | .env файл | SERVER_PORT=9090 в .env |
| 3 | YAML-файл | config.yaml |
| 4 (низший) | Значения по умолчанию | envDefault:"8080" |
| Способ | Сложность | Когда использовать |
|---|---|---|
| Ручная DI (конструкторы) | Низкая | Малые/средние проекты |
| Provider (этот урок) | Средняя | Средние проекты с ленивой инициализацией |
| google/wire | Средняя | Большие проекты (кодогенерация) |
| uber-go/fx | Высокая | Крупные проекты (runtime DI) |
💡 Best practices от сеньоров:
💡 Для Node.js разработчика:
@nestjs/config делает то же самое: загружает .env, валидирует, инжектит.
caarlos0/env — аналог class-validator + class-transformer для env.new Server({ port: 9090 }).gopkg.in/yaml.v3. Такой же синтаксис, как в JS.