Avatar ·

Как правильно управлять конфигурацией в Go: env, флаги или файлы?

Всем привет! Я новичок в Go и пытаюсь разобраться с тем, как правильно хранить настройки приложения. Начал писать небольшой веб-сервер, и у меня возникла проблема: я хочу, чтобы приложение можно было настраивать без перекомпиляции — менять порт, строку подключения к БД, уровень логирования. Пока я делаю так: создаю пакет config, где в init() читаю переменные окружения. Но это кажется костылём, особенно когда настроек много (штук 20). Плюс я не знаю, как делать значения по умолчанию и как удобно передавать конфиг в другие пакеты. Вот мой текущий код: ```go package config import "os" var Port string var DBHost string func init() { Port = os.Getenv("PORT") if Port == "" { Port = "8080" } DBHost = os.Getenv("DB_HOST") if DBHost == "" { DBHost = "localhost" } } ``` Потом в main.go я просто обращаюсь к config.Port. Но это глобальные переменные, и в тестах их сложно переопределять. Подскажите, как в Go принято управлять конфигурацией? Стоит ли использовать YAML/JSON файлы? Или библиотеки типа viper? Или оставить env, но сделать структуру? Мне нужно решение, которое подойдёт и для маленького pet-проекта, и для продакшена. Заранее спасибо! <<>>

Рекомендуемый подход: структура конфигурации + библиотека для загрузки

Ваша интуиция верна — глобальные переменные в init() — плохая практика. В Go принято использовать явные структуры конфигурации, которые можно передавать как зависимости. Это делает код тестируемым и предсказуемым.

Для большинства проектов (от pet-проектов до продакшена) я рекомендую комбинацию: структура + библиотека viper (или envconfig для простых случаев). Вот как это выглядит правильно:

// config/config.go
package config

import (
    "fmt"
    "os"
    "github.com/joho/godotenv"
)

// Config — единая структура всех настроек
type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    Logging  LoggingConfig
}

type ServerConfig struct {
    Port    string `env:"PORT" default:"8080"`
    Host    string `env:"HOST" default:"0.0.0.0"`
}

type DatabaseConfig struct {
    Host     string `env:"DB_HOST" default:"localhost"`
    Port     string `env:"DB_PORT" default:"5432"`
    User     string `env:"DB_USER" default:"postgres"`
    Password string `env:"DB_PASSWORD"`
    Name     string `env:"DB_NAME" default:"mydb"`
}

type LoggingConfig struct {
    Level string `env:"LOG_LEVEL" default:"info"`
    File  string `env:"LOG_FILE"`
}

// Load — читает конфигурацию из .env файла и переменных окружения
func Load(path string) (*Config, error) {
    // Пытаемся загрузить .env файл (если он есть)
    if path != "" {
        if err := godotenv.Load(path); err != nil {
            // Не фатально — просто нет файла
        }
    }

    cfg := &Config{}

    // Заполняем структуру вручную (можно заменить на viper)
    cfg.Server.Port = getEnv("PORT", "8080")
    cfg.Server.Host = getEnv("HOST", "0.0.0.0")
    cfg.Database.Host = getEnv("DB_HOST", "localhost")
    cfg.Database.Port = getEnv("DB_PORT", "5432")
    cfg.Database.User = getEnv("DB_USER", "postgres")
    cfg.Database.Password = getEnv("DB_PASSWORD", "")
    cfg.Database.Name = getEnv("DB_NAME", "mydb")
    cfg.Logging.Level = getEnv("LOG_LEVEL", "info")
    cfg.Logging.File = getEnv("LOG_FILE", "")

    // Валидация обязательных полей
    if cfg.Database.Password == "" {
        return nil, fmt.Errorf("DB_PASSWORD is required")
    }

    return cfg, nil
}

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

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

// main.go
package main

import (
    "log"
    "your-project/config"
    "your-project/app"
)

func main() {
    cfg, err := config.Load(".env")
    if err != nil {
        log.Fatalf("failed to load config: %v", err)
    }

    // Передаём конфиг как зависимость
    server := app.NewServer(cfg)
    if err := server.Run(); err != nil {
        log.Fatal(err)
    }
}

Почему это лучше вашего подхода?

  • Тестируемость: вы можете создать конфиг с любыми значениями в тесте, не меняя глобальные переменные
  • Читаемость: все настройки в одном месте с понятными полями
  • Безопасность: пароли и ключи можно загружать только из env, не храня в репозитории
  • Поддержка: легко добавлять новые поля без init()

Когда использовать viper?

Если у вас много конфигураций (20+ полей), поддержка нескольких форматов (JSON/YAML/TOML) или нужна подгрузка из удалённых источников — используйте github.com/spf13/viper. Он умеет читать теги mapstructure и автоматически маппить в структуру.

Пример с viper:

import "github.com/spf13/viper"

func LoadViper() (*Config, error) {
    viper.SetConfigName("config")    // имя файла без расширения
    viper.SetConfigType("yaml")      // формат
    viper.AddConfigPath(".")         // путь поиска
    viper.AutomaticEnv()             // читать env переменные

    if err := viper.ReadInConfig(); err != nil {
        return nil, err
    }

    var cfg Config
    if err := viper.Unmarshal(&cfg); err != nil {
        return nil, err
    }
    return &cfg, nil
}
<<>>

Альтернатива: только переменные окружения через пакет envconfig

Если вы не хотите подключать тяжёлые библиотеки вроде viper, отличный вариант — пакет github.com/kelseyhightower/envconfig. Он позволяет загружать конфигурацию исключительно из переменных окружения, используя теги структуры.

import "github.com/kelseyhightower/envconfig"

type Config struct {
    Port    int    `envconfig:"PORT" default:"8080"`
    DBHost  string `envconfig:"DB_HOST" default:"localhost"`
    DBPass  string `envconfig:"DB_PASSWORD" required:"true"`
}

func main() {
    var cfg Config
    err := envconfig.Process("myapp", &cfg)
    if err != nil {
        log.Fatal(err)
    }
    // cfg готов к использованию
}

Этот подход идеально подходит для:

  • 12-factor приложений (Heroku, Docker)
  • Микросервисов с небольшим числом настроек
  • Ситуаций, когда конфигурация должна быть строго из окружения (CI/CD)

Минус: нет поддержки файлов. Но для многих проектов это плюс — меньше магии, всё явно задаётся через env. Плюс в тестах вы просто создаёте структуру руками: cfg := &Config{Port: 9090}.