Как работать с конфигурацией в Go: от хаоса к порядку
Каждый разработчик на Go рано или поздно сталкивается с вопросом: «Как правильно хранить и загружать настройки приложения?» Тема настолько популярна, что на Stack Overflow соответствующий вопрос собрал тысячи просмотров и десятки вариантов ответов. И это не случайно: от того, как вы организуете конфигурацию, зависит, насколько легко будет разворачивать приложение, тестировать его и адаптировать под разные окружения.
В этой статье мы разберем самые частые вопросы о конфигурации в Go, рассмотрим реальные примеры кода и выработаем подход, который спасет вас от головной боли при деплое.
Почему конфигурация — это боль?
Представьте: вы написали отличное приложение. Оно работает локально, тесты проходят, коллеги аплодируют. Но когда дело доходит до развертывания на staging и production, начинается ад. Разные базы данных, разные API-ключи, разные уровни логирования. И тут вы понимаете, что хардкодить настройки — плохая идея.
Go предлагает гибкие инструменты для работы с конфигурацией, но новички часто совершают одни и те же ошибки: используют глобальные переменные, путают форматы, забывают про переменные окружения. Давайте разберемся, как делать правильно.
Частый вопрос №1: JSON или YAML?
Самый первый вопрос, который возникает: «В каком формате хранить конфигурацию?» Ответ зависит от контекста, но есть общие рекомендации.
JSON — просто и нативно
Go из коробки умеет работать с JSON через пакет encoding/json. Это отличный выбор для микросервисов и API-шлюзов.
// config.json
{
"port": 8080,
"database": {
"host": "localhost",
"port": 5432,
"name": "myapp"
},
"log_level": "info"
}
// main.go
package main
import (
"encoding/json"
"fmt"
"os"
)
type Config struct {
Port int `json:"port"`
Database Database `json:"database"`
LogLevel string `json:"log_level"`
}
type Database struct {
Host string `json:"host"`
Port int `json:"port"`
Name string `json:"name"`
}
func main() {
file, _ := os.Open("config.json")
defer file.Close()
var config Config
json.NewDecoder(file).Decode(&config)
fmt.Printf("Запускаем сервер на порту %d\n", config.Port)
fmt.Printf("Подключаемся к БД %s:%d/%s\n",
config.Database.Host,
config.Database.Port,
config.Database.Name)
}
YAML — читаемость превыше всего
Для сложных конфигураций с множеством вложенных полей YAML удобнее. Правда, потребуется сторонняя библиотека — например, gopkg.in/yaml.v3.
// config.yaml
server:
port: 8080
timeout: 30s
database:
host: localhost
port: 5432
name: myapp
logging:
level: info
format: json
package main
import (
"fmt"
"gopkg.in/yaml.v3"
"os"
)
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
Logging LoggingConfig `yaml:"logging"`
}
func main() {
data, _ := os.ReadFile("config.yaml")
var config Config
yaml.Unmarshal(data, &config)
fmt.Printf("Таймаут сервера: %s\n", config.Server.Timeout)
}
Совет: Используйте YAML для конфигураций, которые будут читать люди, и JSON — для машинного обмена данными.
Частый вопрос №2: А как же переменные окружения?
В современной разработке (особенно в контейнерах и Kubernetes) переменные окружения стали стандартом де-факто. Они позволяют легко менять настройки без пересборки приложения.
Go поддерживает работу с env через os.Getenv() и os.LookupEnv(). Но вручную парсить каждую переменную — скучно и чревато ошибками. На помощь приходит библиотека github.com/caarlos0/env/v10:
package main
import (
"fmt"
"github.com/caarlos0/env/v10"
)
type Config struct {
Port int `env:"PORT" envDefault:"8080"`
DBHost string `env:"DB_HOST" envDefault:"localhost"`
DBPort int `env:"DB_PORT" envDefault:"5432"`
LogLevel string `env:"LOG_LEVEL" envDefault:"info"`
DebugMode bool `env:"DEBUG_MODE"`
}
func main() {
var config Config
if err := env.Parse(&config); err != nil {
panic(err)
}
fmt.Printf("Порт: %d, Уровень логирования: %s\n",
config.Port, config.LogLevel)
}
Теперь можно запускать приложение с разными настройками:
# Для продакшена
export PORT=9090
export DB_HOST=prod-db.example.com
export LOG_LEVEL=warn
go run main.go
# Для локальной разработки
go run main.go # использует значения по умолчанию
Частый вопрос №3: Как объединить файл и переменные окружения?
Самый частый сценарий в реальных проектах: базовые настройки лежат в файле (например, config.yaml), а чувствительные данные (пароли, API-ключи) передаются через переменные окружения. Как это реализовать элегантно?
Ответ — Viper. Это самая популярная библиотека для конфигурации в Go (более 27k звезд на GitHub). Viper умеет:
- Читать конфиги из файлов (JSON, YAML, TOML, HCL и других)
- Работать с переменными окружения
- Поддерживать удаленные конфигурации (etcd, Consul)
- Устанавливать значения по умолчанию
- Следить за изменениями файлов в реальном времени
package main
import (
"fmt"
"github.com/spf13/viper"
)
func initConfig() {
viper.SetConfigName("config") // имя файла без расширения
viper.SetConfigType("yaml") // формат
viper.AddConfigPath(".") // путь поиска
viper.AddConfigPath("/etc/myapp/")
// Читаем переменные окружения с префиксом MYAPP_
viper.SetEnvPrefix("MYAPP")
viper.AutomaticEnv()
// Значения по умолчанию
viper.SetDefault("server.port", 8080)
viper.SetDefault("logging.level", "info")
if err := viper.ReadInConfig(); err != nil {
// Если файла нет — не страшно, используем env и defaults
fmt.Printf("Конфиг не найден: %s\n", err)
}
}
func main() {
initConfig()
port := viper.GetInt("server.port")
dbHost := viper.GetString("database.host")
logLevel := viper.GetString("logging.level")
fmt.Printf("Сервер на порту %d\n", port)
fmt.Printf("База данных: %s\n", dbHost)
fmt.Printf("Уровень логирования: %s\n", logLevel