Использование конфигурационных файлов в Go: INI
В продолжение темы про конфигурационные файлы, остановлюсь на формате .INI. .INI - формат конфигурационных файлов, применяемый, в основном в ОС Windows. Тем не менее, ничто не мешает его использовать и в других окружениях. Данный формат значительно проще, чем рассмотренные ранее TOML и YAML, но в некоторых (на самом деле большинстве, как мне кажется) случаях, его возможностей может оказаться достаточно.
Синтаксис
INI файлы имеют понятный синтаксис и имеют простой для чтения и изменения человеком формат. Некоторые текстовые редакторы (например, Notepad++) имеют встроенную подсветку синтаксиса INI. Строгого стандарта для формата INI не существует что является причиной того, что некоторые приложения имеют свои “особенности” в подходе к INI файлам:
- массивы в конфигурационных файлах Zend Framework описываются иначе,
- комментарии в конфигурационных файлах Samba могут начинаться как с
;
, так и с#
.
Каноничным символом комментария в INI-файлах считается ;
.
Основные типы
Базовой единицей INI-файла является пара ключ-значение:
key=value
Значения могут иметь различные типы: целое или дробное число, строки, логические значения и массивы этих типов.
Прочие типы
Аналогом таблиц TOML в INI являются секции:
[Section]
key1=value1
key2=3.1415
key3=true
Секции не могут быть вложены в другие секции. Все пары ключ-значение после начала секции относятся к текущей секции до тех пор, пока не будет объявлена новая секция или не будет достигнут конец файла (так же как в TOML).
Работа с INI в Go
Учитывая требование парсить конфигурационный файл в Go-структуру, подходит только одна библиотека на github: go-ini.
API
Помимо парсинга INI-файла в Go-структуру библиотека даёт возможность получать значения свойств и секций используя простой API:
data := `
OutsideKey=Outside Value
[Awesome Section]
StringValue=Hello World!
IntValue=42
`
cfg, err := ini.Load([]byte(data))
if err != nil {
log.Fatal(err)
}
section, _ := cfg.GetSection("")
k, _ := section.GetKey("OutsideKey")
log.Printf("OutsideKey: %s", k)
section, _ = cfg.GetSection("Awesome Section")
k, _ = section.GetKey("StringValue")
log.Printf("Awesome Section.StringValue: %s", k)
k, _ = section.GetKey("IntValue")
log.Printf("Awesome Section.IntValue: %s", k)
Этот же пример, но с маппингом на Go-структуру выглядит довольно элегантно:
type T struct {
OutsideKey string
AwesomeSection AwesomeSection `ini:"Awesome Section"`
}
type AwesomeSection struct {
StringValue string
IntValue int
}
func main() {
data := `
OutsideKey=Outside Value
[Awesome Section]
StringValue=Hello World!
IntValue=42
`
cfg, err := ini.Load([]byte(data))
if err != nil {
log.Fatal(err)
}
t := T{}
err = cfg.MapTo(&t)
if err != nil {
log.Fatal(err)
}
log.Println(t.OutsideKey)
log.Println(t.AwesomeSection)
}
Так же просто как и получение структуры из строки можно загрузить данные из файла (тот же самый, что был в предыдущих статьях про конфиги) и замаппить их на структуру:
cfg, err = ini.Load("config.ini")
if err != nil {
log.Fatal(err)
}
config := Config{}
cfg.MapTo(&config)
Правда здесь возникла одна заминка - “из коробки” библиотека регистрозависимая
и это приводит к тому, что необходимо явно указывать теги ini
, если имена
ключей в конфиге в нижнем регистре, а свойства в структуре в CamelCase:
type Config struct {
ID string `ini:"id"`
Index int `ini:"index"`
GUID string `ini:"guid"`
IsActive bool `ini:"isActive"`
Balance string `ini:"balance"`
Picture string `ini:"picture"`
Age int `ini:"age"`
EyeColor string `ini:"eyeColor"`
Name struct {
First string
Last string
} `ini:"name"`
Company string `ini:"company"`
Email string `ini:"email"`
Phone string `ini:"phone"`
Address string `ini:"address"`
About string `ini:"about"`
Registered string `ini:"registered"`
Latitude string `ini:"latitude"`
Longitude string `ini:"longitude"`
Tags []string `ini:"tags"`
Range []int `ini:"range"`
Friends []string `ini:"friends"`
Greeting string `ini:"greeting"`
FavoriteFruit string `ini:"favoriteFruit"`
SpecialField string `ini:"my_tagged_filed"`
}
Не смертельно, но как-то фу.
Особые возможности библиотеки
Не смотря на то, что стандарт INI не поддерживает вложенные секции, в библиотеке реализована данная функциональность. Сделано это по аналогии с TOML: дочерняя секция отделяется от родительской через точку:
type Parent struct {
Message string
Child Child `ini:"Parent.Child"`
}
type Child struct {
Message string
}
func main() {
data = `
[Parent]
Message = Parent Text
[Parent.Child]
Message = Child Text
`
cfg, err = ini.Load([]byte(data))
if err != nil {
log.Fatal(err)
}
t2 := T2{}
err = cfg.MapTo(&t2)
if err != nil {
log.Fatal(err)
}
log.Println(t2.Parent.Message)
log.Println(t2.Parent.Child.Message)
}
Автоматом, без указания тега ini:"Parent.Child"
почему-то не заработало -
либо я что-то сделал не правильно, либо это ограничения (тогда, правда, не
понятно, в чём профит этой фичи). Другой приятной функцией данной библиотеки
является возможность использовать “рекурсивные значения”. По своей сути это
похоже на anchor-ы в YAML. Значение свойства может включать в себя шаблон
%(key_name)s
, который в рантайме будет заменён значением свойства key_name
текущей или корневой секции:
NAME = ini
[author]
NAME = Unknwon
GITHUB = https://github.com/%(NAME)s
[package]
FULL_NAME = github.com/go-ini/%(NAME)s
cfg.Section("author").Key("GITHUB").String() // https://github.com/Unknwon
cfg.Section("package").Key("FULL_NAME").String() // github.com/go-ini/ini
Итоги
К сожалению, посмотреть на скорость работы библиотеки с большими INI-файлами не удастся, так как формат уже слишком дубовый и сделать адекватный пример без большого количества boilerplate-кода не получится. Как мне кажется, формат INI подходит для простых небольших конфигурационных файлов. Если структура конфигурационного файла становится чуть более сложной, чем простой перечень пар ключ-значение, то становится немного грустно и смотреть приходится уже на что-то более навороченное: YAML, TOML, JSON или, в крайней случае, XML. Исходники, как всегда можно найти в репозитории на bitbucket.