Если вы в IT более 15 минут, то должны знать, что практически каждая программа зависит от некоторых внешних переменных. Например, как я писал в статье про разработку бота для Telegram, к ним можно отнести API- ключи внешних сервисов, используемых в вашем приложении; строки подключения к базе данных; список RSS-лент; список e-mail для каких-либо уведомлений и прочее. Самый простой способ - прописать эти переменные явно в коде, но в этом случае есть недостатки:

  1. Во-первых, если эти данные приватные, то публиковать это во внешние системы управления версиями (github, например) будет невозможно во избежание утечки этих данных;
  2. Во-вторых, если вам необходимо иметь два комплекта переменных (отладочные и релизные), то придётся каждый раз шерстить код в их поисках с последующей заменой на нужные;
  3. В-третьих, справедливо для компилируемых языков программирования, при каждом изменении настроек необходимо пересобирать приложение.

Для того, чтобы избавить себя от этих мучений можно использовать два подхода (которые приходят в голову в первую очередь): хранить переменные в переменных окружения (сомнительный вариант?); использовать конфигурационные файлы. Первый вариант не удовлетворяет условию минимальных телодвижений для переключения между конфигурациями: в этом случае необходимо заменять значения переменных не в коде, а в самих переменных окружения (уже лучше, но все-равно попыхтеть придётся). Для себя я выбрал второй вариант как более простой и менее трудоёмкий в поддержке. В этой и в будущих статьях хочу немного погрузиться в тему того, как и чем можно пользоваться в Go для этой задачи.

TOML: Tom’s Obvious, Minimal Language

Первым рассмотрим формат TOML. Формат далеко не самый известный, но, тем не менее, используется в различных проектах. Типичный TOML файл выглядит как-то так:

# This is a TOML document.

title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
dob = 1979-05-27T07:32:00-08:00 # First class dates

[database]
server = "192.168.1.1"
ports = [ 8001, 8001, 8002 ]
connection_max = 5000
enabled = true

[servers]

  # Indentation (tabs and/or spaces) is allowed but not required
  [servers.alpha]
  ip = "10.0.0.1"
  dc = "eqdc10"

  [servers.beta]
  ip = "10.0.0.2"
  dc = "eqdc10"

[clients]
data = [ ["gamma", "delta"], [1, 2] ]

# Line breaks are OK when inside arrays
hosts = [
  "alpha",
  "omega"
]

Разберёмся, что предоставляет TOML для разработчиков.

Базовый синтаксис

Самый простой TOML-конфиг содержит пары ключ-значение разделённые знаком “=”:

# Комментарии начинаются со знака "#"

string1 = "value 1" # TOML поддерживает строки,
string2 = """value 2 # многострочные строки,
value 3""" 

literal1 = 'value4' # литералы,
literal2 = '''value5  # многострочные литералы,
value6'''

int = 1 # целые числа,

float = 3.1415 # дробные числа,

date = 2016-12-12T03:32:00Z # даты в формате ISO 8601,

bool = true # и булевые значения.

Списки

TOML позволяет описывать в конфигурационных файлах списки (и даже списки списков) простых типов (разделённых через запятую):

nums = [ 1, 2 ]
nested = [[ "a", "b"], [1, 2]]
emails = ["andy@example.com", "ivan@example.com"]

We need to go deeper…

Неоспоримый плюс JSON - возможность описывать объекты. В TOML подобное можно провернуть с помощью таблиц:

[table_name]
firstValue = 1
secondValue = 2

Свойства firstValue и secondValue теперь относятся к таблице table_name. Таблицы могут быть вложенными:

[my_table]
firstValue = 1

[my_table.nested_table]
secondValue = 2

Этот фрагмент TOML файла эквивалентен следующему JSON:

{ 
    "my_table" : {
        "firstValue" : 1,
        "nested_table" : {
            "secondValue" : 2
    }
    }
}

Если таблицы-предки - пусты, то описывать их не обязательно - можно просто указать конечное имя таблицы:

[my_table.nested1.nested2]
value = "hello"

Эквивалентно JSON:

{
    "my_table" : {
        "nested1" : {
            "nested2" : {
                "value" : "hello"
            }
        }
    }
}

Однако, следует учитывать, что всё, что идёт после объявления таблицы до конца файла или до начала новой (или вложенной) таблицы, считается содержимым этой таблицы. Следовательно, располагать таблицы следует в самом конце файла. В TOML так же есть возможность задать список таблиц:

[[products]]
name = "Hammer"
sku = 738594937

[[products]]

[[products]]
name = "Nail"
sku = 284758393
color = "gray"

Фрагмент выше эквивалентен следующему JSON:

{
  "products": [
    { "name": "Hammer", "sku": 738594937 },
    { },
    { "name": "Nail", "sku": 284758393, "color": "gray" }
  ]
}

Во вложенных таблицах так же могут быть таблицы:

[[fruit]]
  name = "apple"

  [fruit.physical]
    color = "red"
    shape = "round"

  [[fruit.variety]]
    name = "red delicious"

  [[fruit.variety]]
    name = "granny smith"

[[fruit]]
  name = "banana"

  [[fruit.variety]]
    name = "plantain"

Эквивалентный JSON:

{
  "fruit": [
    {
      "name": "apple",
      "physical": {
        "color": "red",
        "shape": "round"
      },
      "variety": [
        { "name": "red delicious" },
        { "name": "granny smith" }
      ]
    },
    {
      "name": "banana",
      "variety": [
        { "name": "plantain" }
      ]
    }
  ]
}

Работа с TOML в Go

Для работы с TOML существует множество библиотек для различных платформ и языков программирования. Многие можно найти на странице проекта TOML. Для работы с TOML в Go есть несколько библиотек. Наиболее популярные (по количеству звёздочек на github):

  1. https://github.com/naoina/toml
  2. https://github.com/pelletier/go-toml
  3. https://github.com/BurntSushi/toml

Хотелось бы рассмотреть их чуть более внимательно и сделать небольшой сравнительный анализ по следующим пунктам:

  1. Поддержка стандарта
  2. API
  3. Скорость на больших конфигурационных файлах (~1Mb)

Краткий анализ показал, что библиотека go-toml не устраивает, т.к. предоставляет только лишь возможность исследовать TOML дерево и обращаться к конкретным его узлам (кстати, как показалось, довольно удобным способом). В некоторых задачах этого будет достаточно, но не в нашей. По этой исключим на данный момент данную библиотеку. Итого осталось две библиотеки.

toml от BurntSushi

Поддержка стандарта

Если верить документации, то библиотека умеет в TOML v0.4.0.

API

Во-первых, взглянем на библиотеку toml от BurntSushi. Однажды я уже использовал её для своего небольшого pet-project и тогда она показала себя прекрасно. API библиотеки простейший. Для того, чтобы получить из TOML файла структуру требуется минимум действий:

# config.toml
Age = 25
Cats = [ "Cauchy", "Plato" ]
Pi = 3.14
Perfection = [ 6, 28, 496, 8128 ]
DOB = 1987-07-05T05:45:00Z


package main

import (
    "github.com/BurntSushi/toml"
    "time"
)

type Config struct {
  Age int
  Cats []string
  Pi float64
  Perfection []int
  DOB time.Time
}

func main() {
    var conf Config
    if _, err := toml.DecodeFile("config.toml", &conf); err != nil {
        // обработка ошибки.
    }
}

В результате выполнения данной программы в переменной conf будут данный из нашего config.toml. Помимо метода DecodeFile есть также DecodeReader(r io.Reader, v interface{}) (MetaData, error) и Decode(data string, v interface{}) (MetaData, error), которые позволяют десериализовать не только содержимое файла, но и произвольный io.Reader или, в простейшем случае, строку. Библиотека так же поддерживает struct tags, что является несомненным плюсом:

# toml
my_tagged_filed = "my tagged filed"


type Config struct {
    // Свойство из TOML будет замапплено на это свойство структуры
    // аналогично struct tag-у `json:"whatever"`
    SpecialField string `toml:"my_tagged_filed"`
}

Скорость на больших конфигурационных файлах

Теперь посмотрим, как эта библиотека справляется с относительно большими файлами. Тестовый файл получился размером около 1Мб. Он не покрывает весь стандарт TOML, но основные возможности там есть: таблицы и пары ключ-значение. Краткий фрагмент:

[[Toml]]
  ID = "5888fed8b0546a1eb0cfe5d4"
  Index = 0
  GUID = "d718f1a0-4446-484d-b399-4a0b7e6c6f7b"
  IsActive = false
  Balance = "$1,945.41"
  Picture = "http://placehold.it/32x32"
  Age = 27
  EyeColor = "green"
  Company = "BIZMATIC"
  Email = "dolores.alvarado@bizmatic.com"
  Phone = "+1 (890) 429-2744"
  Address = "227 Gallatin Place, Golconda, Delaware, 4021"
  About = "Minim ea est laborum occaecat eu tempor quis ea laborum. Ipsum occaecat esse laboris consequat amet dolor velit non qui ut do cupidatat sint laborum. Consequat elit nisi aute proident in enim nulla ullamco et. Lorem fugiat amet excepteur amet nostrud quis. Incididunt culpa fugiat proident consectetur sit do officia tempor laborum. Magna minim reprehenderit velit mollit magna sunt in in proident proident Lorem minim dolor."
  Registered = "Sunday, April 27, 2014 12:54 AM"
  Latitude = "20.01574"
  Longitude = "-65.423255"
  Tags = ["sit", "occaecat", "cupidatat", "sint", "aliquip", "fugiat", "labore", "in", "ut", "dolore"]
  Range = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299]
  Greeting = "Hello, Dolores! You have 8 unread messages."
  FavoriteFruit = "strawberry"
  [Toml.Name]
    First = "Dolores"
    Last = "Alvarado"

  [[Toml.Friends]]
    ID = 0
    Name = "Jacquelyn Sutton"
    Phone = "+1 (976) 547-2070"
    Company = "SPORTAN"
    Email = "undefined.undefined@sportan.info"

  [[Toml.Friends]]
    ID = 1
    Name = "Navarro Dixon"
    Phone = "+1 (970) 539-3585"
    Company = "QUILITY"
    Email = "undefined.undefined@quility.io"

И далее еще на 40+ тысяч строк. Для профилирования скорости я использовал библиотеку stopwatch:

import (
    burntToml "github.com/BurntSushi/toml"
    naoinaToml "github.com/naoina/toml"
    "log"
    "io/ioutil"
    "github.com/fatih/stopwatch"
)

func main() {
    sw := stopwatch.New()
    sw.Start(0)
    for i := 0; i < 10; i++ {        parseLargeFile_Burnt()     }     log.Printf("Total: %s", sw.ElapsedTime().String()) // >> **Total: 2.4901086s**
}

func parseLargeFile_Burnt() {
    large := Toml{}
    if _, err := burntToml.DecodeFile("large.toml", &large); err != nil {
       log.Fatal(err)
    }
}

Данный файл, судя по моим замерам, обрабатывается приблизительно за 249 мс на одну итерацию (2.49 секунд на 10 итераций).

toml от naoina

Поддержка стандарта

По заверениям автора, библиотека поддерживает ту же версию стандарта, что и предыдущая. Поверим и проверять не будем.

API

Библиотека toml от naoina имеет схожую функциональность и так же позволяет получить структуру из TOML-файла. API похож на API предыдущей библиотеки:

package main

import (
    "github.com/naoina/toml"
    "time"
)

type Config struct {
  Age int
  Cats []string
  Pi float64
  Perfection []int
  DOB time.Time
}

func main() {
    var conf Config
    err = naoinaToml.Unmarshal(bytes, &config)
    if err != nil {
        // обработка ошибки.
    }
}

Недостатком (в сравнении с библиотекой от BurntSushi) можно записать меньшее количество способов получить структуру из TOML: только из массива байт, как в примере выше. К достоинствам можно отнести возможность построить AST дерево TOML-файла при помощи метода Parse([]byte) (*ast.Table, error). Вероятно, это кому-то потребуется, но мне до сих пор такая возможность была не нужна.

Скорость на больших конфигурационных файлах

Тот же самый файл на 40+ тысяч строк, на том же самом ноутбуке обрабатывается почти в 5 (пять!) раз медленнее:

...
sw.Start(0)
for i := 0; i < 10; i++ {
       parseLargeFile_Naoina1()
}
log.Printf("Total: %s", sw.ElapsedTime().String()) // **>> Total: 9.6110093s**
...

func parseLargeFile_Naoina1() {
 large_1 := Toml{}
 var bytes []byte
 bytes, err := ioutil.ReadFile("large.toml")
 if err != nil {
 log.Fatal(err)
 }

 if err := naoinaToml.Unmarshal(bytes, &large_1); err != nil {
 log.Fatal(err)
 }
}

Первой мыслью было, что проблема в том, что на каждую итерацию файл считывается с диска повторно. Добавив вариант с кешированием считанных из файла данных, результат остался практически тот же:

...
sw.Start(0)
bytes, err = ioutil.ReadFile("large.toml")
if err != nil {
    log.Fatal(err)
}
for i := 0; i < 10; i++ {
    parseLargeFile_Naoina2(bytes)
}
log.Printf("Total: %s", sw.ElapsedTime().String()) // **>> Total: 9.6035087s**
...

func parseLargeFile_Naoina2(bytes []byte) {
       large_1 := Toml{}
       if err := naoinaToml.Unmarshal(bytes, &large_1); err != nil {
              log.Fatal(err)
       }
}

Видимо, проблема в другом месте (в самой библиотеке) и разбираться сейчас с ней нет ни времени, ни желания. К счастью для библиотеки, задача с чтением больших файлов скорее исключение из правил, чем то, с чем приходится сталкиваться ежедневно.

Итого

  • Обе библиотеки похожи функционально и по API;
  • Скорость на “обычных” конфигурационных файлах приемлемая (примерно 1 микросекунда и 2 миллисекунды для burnt и naoina соответственно);
  • При обработке относительно большого файла, библиотека от naoina ощутимо проигрывает библиотеке от burnt.

Последний пункт скорее придирка, чем реальная проблема в большинстве случаев. Таким образом, обе библиотеки вполне пригодны для использования и выбор стоит только лишь за разработчиком. Весь код можно найти на bitbucket. При подготовке статьи были полезными следующие ссылки:

  1. https://blog.gopheracademy.com/advent-2014/reading-config-files-the-go-way/
  2. https://npf.io/2014/08/intro-to-toml/
  3. beta.json-generator.com
  4. https://mholt.github.io/json-to-go/