dotzero

↑ ↑ ↓ ↓ ← → ← → B A Start

Релизим Go приложения на Github

Написав очередную поделку на Go решил поделиться с сообществом, а чтобы никто не компилировал ее у себя дома можно воспользоваться тем скудным механизмом релизов, что предлагает Github. Но собирать и заливать артефакты сборки руками это не наш путь, ищем готовые решения. Собираем список качеств для такого инструмента:

  • Сборка бинарников под Linux/Mac, с установкой значений для ldflags флагов;
  • Автоматическая генерация описания релиза из сообщений к коммитам;
  • Единообразие - чтобы не собирать релиз из палок и веревок.

Вооружившись списком идем на Github в поисках подходящего инструмента.

aktau/github-release

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

Имея такой скудный набор функций заставляет помнить тонну флагов, без которых отказывается работать. Не умеет сам билдить и не предлагает примеров по построению такого процесса. Выбирать такой инструмент смысла нет, несмотря на такое количество звезд на Github.

$ github-release release \
    --user aktau \
    --repo gofinance \
    --tag v0.1.0 \
    --name "the wolf of source street" \
    --description "Not a movie, contrary to popular opinion. Still, my first release!" \

$ github-release upload \
    --user aktau \
    --repo gofinance \
    --tag v0.1.0 \
    --name "gofinance-osx-amd64" \
    --file bin/darwin/amd64/gofinance

tcnksm/ghr

Сам автор сравнивает свой инструмент с предыдущим кандидатом говоря, что его поделка еще проще. Местами он проще и умеет читать из файла .git/config название репозитория для создания релиза, а еще умеет заливать все файлы разом в параллель. На этом плюсы заканчиваются, сборка релиза и описание остаются без внимания.

$ ghr -b "Release description" v0.1.0 pkg/

c4milo/github-release

Похож на ghr но не умеет читать настройки из git. Единственный из претендентов кто решил помочь советом как из gox и Makefile сделать готовый релиз и залить на Github, собрав при этом описание из сообщений к коммитам.

$ gox -ldflags "-X main.Version=v0.1.0" \
    -osarch="darwin/amd64" \
    -os="linux" \
    -output "dist/{{.Dir}}_$(VERSION)_{{.OS}}_{{.Arch}}/$(NAME)" \
    ./...

$ github-release \
    c4milo/release \
    v0.1.0 \
    "$$(git rev-parse --abbrev-ref HEAD)" \
    "**Changelog**<br/>$$(git log $$comparison --oneline --no-merges)" \
    "dist/*";

goreleaser/goreleaser

Просто комбайн, умеет много и при этом тщательно это скрывает. Редкий случай когда такой монстр может быть таким простым в управлении. Конфигурация по сборке хранится в файле .goreleaser.yml, а потому для сборки флагами можно пренебречь. Умеет собирать, запаковывать и заливать билды под все платформы, генерить описание из сообщений к коммитам и даже делать формулы для Homebrew.

Но есть и минусы - излишня любовь к саморекламе, описания к релизам будут содержать подпись «Automated with GoReleaser…». А еще у него сносит башню если текущий git HEAD не указывает на тег с номеров версии. Но если смириться с этим, то вариантов выбирать что-то отличное от goreleaser просто нет.

Ledger HW.1

На волне криптохайпа захотелось приобрести себе аппаратный Bitcoin кошелек. Но прикинув цены на Trezor и Ledger Nano S купил младшую модель Ledger HW.1 за $20, пользоваться я им конечно не буду.

Главная фишка аппаратных кошельков хранение приватных ключей без подключения к интернету и апаратная рандомизация seed при создании ключа. Большинство из них имеют экран для отображения информации о транзакциях и коды вторых факторов при подключении к компьютеру, Ledger HW.1 конечно не имеет.

Ledger HW.1

В упаковке помимо usb стика лежала карта второго фактора авторизации и бумажка для записи мнемонической фразы от кошелька. Вместо инструкции по применению лежала памятка с адресом https://www.ledgerwallet.com/start.

Ledger HW.1

Первое разочарование постигло когда оказалось, что для взаимодействия с кошельком кроме расширения для Chrome ничего другого нет. Кошелек рассчитан на простого обывателя, а параноики с Tails и Tor должны страдать. Можно конечно воспользоваться библиотекой на питоне, которая умеет читать и писать на стик, но никакой вменяемой документации или примеров использования нет.

Поскольку на сайте Ledger кроме красивых картинок невозможно найти список коинов, которые поддерживает HW.1, то второе разочарование постигло когда подключенный стик просил выбрать Bitcoin или Bitcoin Cash и оказалось, что ничего другого он не умеет, а все разнообразие альтоинов поддерживает только более продвинутая (читай — дорогая) линейка кошельков. Выбираем Bitcoin segwit2x, вводим второй фактор с карточки и кошелек готов к работе.

Ledger HW.1

Выводы такие, за $20 можно поиграться и посмотреть, что из себя представляют аппаратные кошельки. Но если рассматривать его как параноидальное место для хранения накоплений, то лучше приобрести Trezor или KeepKey.

Имплементация LRU кэша на Go

LRU: Least Recently Used — алгоритм кэширования, при котором вытесняются значения, которые дольше всего не запрашивались. Алгоритмическая сложность O(1), а потому кеш работает очень быстро и используется в memcached.

Кеш имеет очередь фиксированного размера. Когда новый элемент попадает в кеш, то добавляется в начало очереди. При запросе элемента очередь выталкивает элемент в начало, а если нужно освободить место, то из вытесняется последний элемент.

Свой memcached на Go

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

import (
    "container/list"
)

type Item struct {
    Key   string
    Value interface{}
}

type LRU struct {
    capacity int
    items    map[string]*list.Element
    queue    *list.List
}

func NewLru(capacity int) *LRU {
    return &LRU{
        capacity: capacity,
        items:    make(map[string]*list.Element),
        queue:    list.New(),
    }
}

Структура LRU содержит поле с количеством ячеек, поле двусвязного списка *list.List и поле для хранения хеш-таблицы map[string]*list.Element. А Item содержит поля для хранения ключа и значения кэшируемого элемента. Функция конструктор NewLru инициализирует LRU и возвращает ссылку на экземпляр.

Сохраняем значение в кэше

При сохранении элемента в кеше, инициализируем новую структуру Item и добавляем ее в начало очереди c.queue.PushFront(item). Возвращенный очередью *Element добавляем в хеш-таблицу, где ключ это идентификатор записи, а значение это ссылка на элемент очереди.

func (c *LRU) Set(key string, value interface{}) bool {
    if element, exists := c.items[key]; exists == true {
        c.queue.MoveToFront(element)
        element.Value.(*Item).Value = value
        return true
    }

    if c.queue.Len() == c.capacity {
        c.purge()
    }

    item := &Item{
        Key:   key,
        Value: value,
    }

    element := c.queue.PushFront(item)
    c.items[item.Key] = element

    return true
}

Перед добавлением в очередь проверяем нет ли уже такого ключа в хеш-таблице и если есть, то заменяем значение на новое и двигаем в начало очереди c.queue.MoveToFront(element).

Если количество элементов очереди равно максимальному количеству ячеек, то пора выбросить последний элемент из очереди и ключ из хеш-таблицы вызвав функцию purge().

func (c *LRU) purge() {
    if element := c.queue.Back(); element != nil {
        item := c.queue.Remove(element).(*Item)
        delete(c.items, item.Key)
    }
}

Получаем значение из кэша

При запросе элемента из кеша, ищем соответствие ключа в хеш-таблице и при нахождении получаем значение элемента через каст значения на структуру element.Value.(*Item).Value. Перед возвращением перемещаем элемент в начало очереди.

func (c *LRU) Get(key string) interface{} {
    element, exists := c.items[key]
    if exists == false {
        return nil
    }
    c.queue.MoveToFront(element)
    return element.Value.(*Item).Value
}

Дальше можно добавить mutex и сделать функцию потоко-безопасной. А еще можно заменить количество ячеек на размер кеша по объему памяти хранимых сущностей.

Среднеквадратическое отклонение

Расскажу о среднеквадратическом отклонении на примере собак. Имея группу собак рост которых 600, 470, 170, 430 и 300 мм. Как узнать какие из этих собак большие, какие маленькие, а какие можно отнести к средним? Тут на помощь приходит среднеквадратическое отклонениеσ (греческая буква сигма).

Формула очень проста: это квадратный корень из дисперсии случайной величины. Что такое дисперсия? Это среднее арифметическое квадратов разностей от среднего арифметического.

А теперь конкретно на примере наших собак, все вычисления буду писать на python без использования numpy. Первым делом находим среднее арифметическое всех элементов:

dogs = [600, 470, 170, 430, 300]
average = sum(dogs) / len(dogs)
# 394

Теперь надо посчитать дисперсию, для этого из каждой высоты собаки вычитаем среднее арифметическое всех элементов, сумируем и делим на количество элементов:

variance = sum([(n-average)**2 for n in dogs]) / len(dogs)
# 21704

Последним шагом извлекаем квадратный корень из дисперсии:

standard_deviation = variance ** 0.5
# ~147

Таким образом имея среднеквадратическое отклонение (147) и среднее арифметическое (394) можно сказать, что верхний порог для средней собаки — 394 + 147 = 541, а значит собака ростом 600 мм — большая. Для маленьких собак этот порог — 394 - 147 = 247, а значит собака ростом 170 мм - маленькая.

Но что делать если собак очень много и их количество постоянно растет? Обычный подход к вычислению тут не подойдет. В таком случае необходимо заменить среднее арифметическое математическим ожиданием при вычислении дисперсии.

Если вернуться к нашим собакам и мы считаем, что эти 5 собак лишь кусок от большой популяции собак, то при вычислении дисперсии необходимо делить не на число элементов, а на число элементов минус 1.

variance = sum([(n-average)**2 for n in dogs]) / (len(dogs) - 1)
# 27130
standard_deviation = variance ** 0.5
# ~164

Lindau