В конфигурационном файле мы будем использовать следующие сторонние библиотеки:

  • viper - одна из лучших библиотек для создания конфигурационных файлов.
  • pflag - библиотека для работы с флагами.
  • enumflag - в нашем случае данная библиотека будет использоваться для валидации входящих флагов.

Конфигурационный файл должен последовательно брать значения переменных исходя из выстроенного приоритета:

  1. Для начала будут использованы значения переменных по умолчанию.
  2. Затем, если есть конфигурационный yaml файл, то значения переменных конфига перезапишутся значениями из этого файла. Если нет yaml файла, то будет ошибка.
  3. Если есть переменные в окружении, то значения переменных конфига перезапишутся переменными из окружения.
  4. И наконец, если заданы флаги, то значения переменных конфига перезапишутся значениями заданными во флагах.

Заполняем конфиг значениями по умолчанию

Для указания команд по умолчанию в пакете 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"))