Avatar ·

Как правильно работать с конфигурацией в Go: ENV, JSON, YAML или флаги?

Всем привет! Я начинающий Go-разработчик, пишу свой первый микросервис для обработки заказов. Столкнулся с проблемой: не могу понять, как правильно организовать хранение конфигурационных параметров (порт сервера, строка подключения к БД, URL внешнего API, таймауты и т.д.). Сейчас я просто захардкодил всё в main.go: ```go package main import ( "fmt" "net/http" ) func main() { port := ":8080" dbConn := "postgres://user:pass@localhost:5432/orders?sslmode=disable" apiURL := "https://api.example.com" fmt.Println("Starting server on", port) http.ListenAndServe(port, nil) } ``` Понимаю, что это ужасно: при смене окружения (dev/staging/prod) нужно перекомпилировать бинарник, а пароли в коде — это вообще катастрофа безопасности. Видел, что в Go используют разные подходы: флаги командной строки, переменные окружения, JSON/YAML/Toml-файлы. Но какой выбрать? Как объединить несколько источников? Как сделать, чтобы конфигурация была типобезопасной? Буду благодарен за примеры и best practices! <<>>

Правильный подход к конфигурации в Go

Проблема, которую вы описали, очень распространена. В Go есть несколько устоявшихся паттернов, и лучший из них — комбинированный подход: используйте переменные окружения (ENV) как основной источник для чувствительных данных и конфигурационных файлов (JSON/YAML) для остальных параметров, с поддержкой флагов для переопределения.

Вот рекомендуемая структура, которая решает все ваши проблемы:

// config.go
package config

import (
    "encoding/json"
    "flag"
    "fmt"
    "os"
    "time"
)

// Config — типобезопасная структура конфигурации
type Config struct {
    Server   ServerConfig   `json:"server"`
    Database DatabaseConfig `json:"database"`
    External ExternalConfig `json:"external"`
}

type ServerConfig struct {
    Port    int           `json:"port"`
    Timeout time.Duration `json:"timeout"`
}

type DatabaseConfig struct {
    Host     string `json:"host"`
    Port     int    `json:"port"`
    User     string `json:"user"`
    Password string `json:"-"` // "-" — не выводим в лог/JSON
    DBName   string `json:"dbname"`
}

type ExternalConfig struct {
    APIURL string `json:"api_url"`
}

// Load загружает конфигурацию с приоритетом: флаги > ENV > файл
func Load(configPath string) (*Config, error) {
    cfg := &Config{}
    
    // 1. Загружаем из файла (если указан)
    if configPath != "" {
        if err := loadFromFile(configPath, cfg); err != nil {
            return nil, fmt.Errorf("load file: %w", err)
        }
    }
    
    // 2. Переопределяем из ENV
    loadFromEnv(cfg)
    
    // 3. Переопределяем из флагов (если запуск с ними)
    loadFromFlags(cfg)
    
    return cfg, nil
}

func loadFromFile(path string, cfg *Config) error {
    data, err := os.ReadFile(path)
    if err != nil {
        return err
    }
    return json.Unmarshal(data, cfg)
}

func loadFromEnv(cfg *Config) {
    // Пример: SERVER_PORT=9090
    if v := os.Getenv("SERVER_PORT"); v != "" {
        fmt.Sscanf(v, "%d", &cfg.Server.Port)
    }
    if v := os.Getenv("DB_PASSWORD"); v != "" {
        cfg.Database.Password = v
    }
}

func loadFromFlags(cfg *Config) {
    port := flag.Int("port", cfg.Server.Port, "Server port")
    flag.Parse()
    cfg.Server.Port = *port
}

Как это работает:

  • Файл config.json — хранит базовые настройки (порт, таймауты, URL). Не содержит паролей.
  • ENV — для секретов (пароль БД, API-ключи) и переопределения настроек под окружение.
  • Флаги — для быстрого тестирования или временных изменений.

Пример файла config.json:

{
    "server": {
        "port": 8080,
        "timeout": "30s"
    },
    "database": {
        "host": "localhost",
        "port": 5432,
        "user": "app_user",
        "dbname": "orders"
    },
    "external": {
        "api_url": "https://api.example.com/v2"
    }
}

Использование в main.go:

func main() {
    cfg, err := config.Load("config.json")
    if err != nil {
        log.Fatalf("Failed to load config: %v", err)
    }
    
    // Собираем строку подключения
    dsn := fmt.Sprintf(
        "postgres://%s:%s@%s:%d/%s?sslmode=disable",
        cfg.Database.User,
        cfg.Database.Password,
        cfg.Database.Host,
        cfg.Database.Port,
        cfg.Database.DBName,
    )
    
    // Запускаем сервер
    addr := fmt.Sprintf(":%d", cfg.Server.Port)
    log.Printf("Starting server on %s", addr)
    // ...
}

Почему это лучший подход:

  • Типобезопасность — компилятор проверяет поля структуры
  • Безопасность — пароли не попадают в репозиторий (используйте .gitignore для config.json с секретами)
  • Гибкость — легко менять поведение без перекомпиляции
  • 12 Factor App — соответствует принципам хранения конфигурации в окружении

Совет: Для продакшена используйте библиотеку github.com/spf13/viper — она реализует всё это из коробки и поддерживает YAML, TOML, etcd и другие источники. Но для понимания механики полезно написать свой велосипед один раз.

<<>>

Альтернативный подход: только переменные окружения

Если ваш проект небольшой или вы следуете принципам 12 Factor App строго, можно обойтись одними переменными окружения. Это особенно удобно для Docker/Kubernetes.

Пример утилитарной функции для парсинга ENV:

package config

import (
    "os"
    "strconv"
    "time"
)

type Config struct {
    Port    int           `env:"PORT" default:"8080"`
    Timeout time.Duration `env:"TIMEOUT" default:"30s"`
    DBURL   string        `env:"DATABASE_URL"`
}

func FromEnv() (*Config, error) {
    cfg := &Config{}
    
    cfg.Port = getEnvInt("PORT", 8080)
    cfg.Timeout = getEnvDuration("TIMEOUT", 30*time.Second)
    cfg.DBURL = getEnv("DATABASE_URL", "")
    
    if cfg.DBURL == "" {
        return nil, errors.New("DATABASE_URL is required")
    }
    
    return cfg, nil
}

func getEnv(key, fallback string) string {
    if v := os.Getenv(key); v != "" {
        return v
    }
    return fallback
}

func getEnvInt(key string, fallback int) int {
    if v := os.Getenv(key); v != "" {
        if i, err := strconv.Atoi(v); err == nil {
            return i
        }
    }
    return fallback
}

Этот подход минималистичен и идеален для контейнерных сред. Недостаток — при большом количестве параметров код становится громоздким. В таком случае рекомендую использовать библиотеку github.com/caarlos0/env/v10 для автоматического маппинга ENV на структуру через теги.