В конфигурационном файле мы будем использовать следующие сторонние библиотеки:
- 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"))