В продолжение темы про конфигурационные файлы, остановлюсь на формате .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.

Ссылки

  1. https://en.wikipedia.org/wiki/INI_file
  2. https://github.com/go-ini/ini