Как правильно работать с конфигурацией в Go: ENV, JSON, YAML или флаги?
Правильный подход к конфигурации в 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 на структуру через теги.
Ответов пока нет.
Войдите чтобы оставить ответ
Блоги