Как правильно управлять конфигурацией в Go: env, флаги или файлы?
Рекомендуемый подход: структура конфигурации + библиотека для загрузки
Ваша интуиция верна — глобальные переменные в 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}.
Ответов пока нет.
Войдите чтобы оставить ответ