В конфигурационном файле мы будем использовать следующие сторонние библиотеки:
- viper - одна из лучших библиотек для создания конфигурационных файлов.
- pflag - библиотека для работы с флагами.
- enumflag - в нашем случае данная библиотека будет использоваться для валидации входящих флагов.
Конфигурационный файл должен последовательно брать значения переменных исходя из выстроенного приоритета:
- Для начала будут использованы значения переменных по умолчанию.
- Затем, если есть конфигурационный
yaml
файл, то значения переменных конфига перезапишутся значениями из этого файла. Если нетyaml
файла, то будет ошибка. - Если есть переменные в окружении, то значения переменных конфига перезапишутся переменными из окружения.
- И наконец, если заданы флаги, то значения переменных конфига перезапишутся значениями заданными во флагах.
Заполняем конфиг значениями по умолчанию
Для указания команд по умолчанию в пакете viper
есть функция SetDefault
,
достаточно указать переменную и значение по умолчанию для нее.
Вот пример для логгера, в данном случае мы установили уровень логирования в info
:
viper.SetDefault("logger.level", "info")
Первым параметром передается ключ, соответстующий значениям тегов mapstructure
- Logger
и Level
.
Заполняем конфиг из yaml
файла
Создадим структуру для хранения конфигурации, которая будет содержать основные структуры и переменные нашего приложения.
Для примера ограничимся одной структурой - Log
.
// Config represents the config file.
type Config struct {
Logger Log
}
Опишем вложенную структуру:
// Log represents log section in config file.
type Log struct {
Level string
Format string
}
Свяжем поля структур с одноименными переменными в yaml
файле. Создадим config.yaml
со следующим содержимым:
---
log:
level: "info"
format: "json"
Здесь мы задаем значения для полей структуры Log
.
Для связи переменных из yaml
файла с полями структур, используется тег mapstructure
. Вот как это будет выглядеть:
// Config represents the config file.
type Config struct {
Logger Log `mapstructure:"log"`
}
// Log represents log section in config file.
type Log struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
}
Таким образом мы связали поля структур с переменными в yaml
файле.
Создадим конструктор, в котором реализуем работу с yaml
файлом:
func New() (*Config, error)
Укажем имя, тип и путь к нашему конфигурационному файлу.
Пропишем путь (AddConfigPath
) - их может быть несколько. Затем имя yaml
файла (SetConfigName
) и тип файла - yaml
:
viper.AddConfigPath("configs")
viper.AddConfigPath("/etc/example-gorilla-rest-api")
viper.SetConfigName("config")
viper.SetConfigType("yaml")
Найдем и загрузим конфигурационный файл, обработав тип ошибки, чтобы продолжать работу, если конфигурационный файл не найден.
// Discover and load the configuration file from disk.
if err := viper.ReadInConfig(); err != nil {
switch err.(type) {
case viper.ConfigFileNotFoundError:
log.Println("No Config file found, loaded config from Environment - Default path ./conf")
default:
log.Fatalf("Error when Fetching Configuration - %s", err)
}
}
Осталось распарсить значения из yaml
файла в переменные конфига:
var cfg Config
err := viper.Unmarshal(&cfg)
if err != nil {
return nil, err
}
Заполняем конфиг из окружения
Пакет viper
располагает хорошим функционалом для работы с переменными окружения.
Для начала следует задать префикс для переменных окружения вашего проекта при помощи SetEnvPrefix
:
viper.SetEnvPrefix("EGRA")
Также стоит упомянуть про функцию, которая позволяет устанавливать соответствие между именами переменных окружения и ключами, используемыми в конфигурационных файлах.
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
Здесь мы заменяем “.” на “_” для переменных окружения.
Осталось подтянуть переменные из окружения, можно прописывать по одной, используя viper.BindEnv("ENV_VARIABLE")
,
но лучше воспользуемся goviper.AutomaticEnv()
- так мы автоматически подтянем все переменные из окружения:
_ = viper.AutomaticEnv()
Заполняем конфиг через флаги
Самая приоритетная часть, удобство флагов заключается в том, что даже если мы уже собрали приложение и хотим запустить его с другими параметрами - нам не надо его пересобирать, достаточно просто запустить исполняемый файл, указав параметры через флаги.
Для указания флага будем использовать упомянутый выше пакет pflag . Создадим флаги для указания адреса и порта базы данных, при этом адрес у нас - строка, а порт - число:
pflag.StringVarP(&databaseHost, "database.host", "H", "localhost", "database host")
pflag.IntVarP(&databasePort, "database.port", "P", 5432, "database port")
Разберем параметры передаваемые в эти функции: первый параметр - переменная,
в которую будет присвоен флаг, вторая - имя флага, третье - сокращенное имя фалага,
четвертое - значение по умолчанию, пятое - описание для справки “помощь”,
которую можно вызвать через флаг -h
или --help
.
Задается флаг в терминале при помощи имени или сокращенного имени (3 и 4 параметры в функции). Например --database.host=127.0.0.1
или -H=127.0.0.1
.
Рассмотрим специфику при создании флага для логгера (log/slog
),
она заключается в том, что мы должны принимать ограниченное количество определенных параметров (debug, info, warn, error
),
для этого воспользуемся функцией New
из пакета enumflag
.
В параметры функции передадим переменную для присвоения значения из флага, имя флага,
map
для сопоставления входящего значения с допустимыми значениями и последним параметром установим чувствительность к регистру.
Данная функция передается первым параметром в функцию VarP
из пакета pflag
, вот как это будет выглядеть:
var logLevel slog.Level
var logLevelIds = map[slog.Level][]string{
slog.LevelDebug: {"debug"},
slog.LevelInfo: {"info"},
slog.LevelWarn: {"warning", "warn"},
slog.LevelError: {"error"},
}
pflag.VarP(
enumflag.New(&logLevel, "log.level", logLevelIds, enumflag.EnumCaseSensitive),
"log.level", "l", "log level",
)
Осталось добавить привязку флага при помощи функции BindPFlag
, поиск флага происходит при помощи функции Lookup
:
viper.BindPFlag("log.level", pflag.Lookup("log.level"))