↑ ↑ ↓ ↓ ← → ← → B A Start

Кибер настольный календарь

Типичный день в энтерпрайзе чуть менее, чем полностью состоит из встреч. В офисе об этих встречах напоминает не только Microsoft Exchange но и члены команды.

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

Raspberry Pi

Для этого взял Raspberry Pi Zero W, прицепил экран и написал скрипт который раз в 5 минут заходит на корпоративный сервер Exchange, достанет ближайшую встречу и выводит на экран ее название и человекопонятное описание когда она начнется.

Raspberry Pi

Календарь я хотел сделать приближенным к аналоговым предметам, а потому взял E Ink экран, который не светится в темноте и не раздражает глаза.

Waveshare 2.13inch e-Paper HAT я купил год назад на Amazon и он все это время лежал без дела. Китайцы из Waveshare делают экраны совместимые с Raspberry Pi и любезно предоставляют готовый SDK на Python.

Raspberry Pi

Итоговый проект и инструкция по установке выложил на github и подробно рассматривать не буду. Если кратко, то функция next_meeting достает ближайшую встречу, а функция get_image формирует изображение, которое выводится на экран.

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

Как работает встраивание ошибок в Go

Обработка ошибок в Go концептуально прозрачна но вызывает массу вопросов и негодования у людей переходящих на Go с ООП языков. И первое с чем такие люди сталкиваются это отсутствие вложенности ошибок. Чтобы как-то смягчить эту боль в заднице Dave Cheney написал библиотеку для оборачивания ошибок и реализуя некое подобие наследования.

К версии 1.13 разработчики устали слушать нытье и насыпали немного синтаксического сахара добавив новую структуру для оборачивания ошибок и две новых функции для работы с ошибками. Вот как это работает под капотом. Когда мы создаем обычную ошибку, то фактически инициализируем следующую структуру.

type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

А если оборачиваем ошибку используя вызов fmt.Errorf("%w", err), то возвращаемая ошибка получит уже другую структуру, содержащую указатель на оборачиваемую ошибку и метод Unwrap() для доступа к ней.

type wrapError struct {
	msg string
	err error
}

func (e *wrapError) Error() string {
	return e.msg
}

func (e *wrapError) Unwrap() error {
	return e.err
}

С такими ошибками можно делать ровно два действия, сравнить со значением и с типом (не совсем). Понять как это работает проще всего на примере. Возьмем абстрактный микросервис, которых ходит в другой микросервис через RPC клиент по http.

Handler → Middleware → RPC Client → HTTP Client

Ошибка может произойти как на уровне http клиента, так и в постобработке полученного ответа. При этом логирование ошибок происходит на уровне handler’а, а остальные уровни только оборачивают ошибки и пробрасывают наверх. Хватит теории, давай код.

package main

import (
	"fmt"
	"net/http"
)

type httpError struct {
	Code    int
	Message string
}

func (e *httpError) Error() string {
	return fmt.Sprintf("%d: %s", e.Code, e.Message)
}

type rpcError struct {
	Method string
	err    error
}

func (e *rpcError) Error() string {
	return fmt.Sprintf("%s error: %s", e.Method, e.err.Error())
}

func (e *rpcError) Unwrap() error {
	return e.err
}

func main() {
	httpErr := &httpError{
		Code:    http.StatusInternalServerError,
		Message: http.StatusText(http.StatusInternalServerError),
	}
	rpcErr := &rpcError{
		Method: "getEnterpriseResourceV1",
		err:    httpErr,
	}
	middlewareErr := fmt.Errorf("middleware: %w", rpcErr)
	handlerErr := fmt.Errorf("handler: %w", middlewareErr)

	fmt.Printf("%q\n", handlerErr)
	// handler: middleware: getEnterpriseResourceV1 error: 500: Internal Server Error
}

Тут происходит имитация ошибки на уровне http клиента с последующим оборачиванием в rpc ошибку, а потом в middleware и handler. Конечная ошибка выглядит как набор вложенных друг в друга структур.

fmt.wrapError{
    msg: "handler: middleware: getEnterpriseResourceV1 error: 500: Internal Server Error",
    err: &fmt.wrapError{
        msg: "middleware: getEnterpriseResourceV1 error: 500: Internal Server Error",
        err: &rpcError{
            Method: "getEnterpriseResourceV1",
            err: &httpError{
                Code:    500,
                Message: "Internal Server Error",
            },
        },
    },
}

Имея такую матрешку попробуем выяснить какая ошибка была изначально и какие этапы оборачивания она прошла. Для этого воспользуемся функцией сравнения ошибок по значению errors.Is(err, target error). Это функция рекурсивно сравнивает значение ошибки с target вызывая .Unwrap() для продвижения вглубь матрешки.

errors.Is(handlerErr, handlerErr)
// true т.к эквивалент ==
errors.Is(handlerErr, middlewareErr)
// true т.к handlerErr.Unwrap() == middlewareErr
errors.Is(handlerErr, rpcErr)
// true т.к handlerErr.Unwrap().Unwrap() == rpcErr
errors.Is(handlerErr, httpErr)
// true т.к handlerErr.Unwrap().Unwrap().Unwrap() == httpErr

errors.Is(handlerErr, fmt.Errorf("generic")) // false
errors.Is(handlerErr, &unknownError{})       // false
errors.Is(handlerErr, &rpcError{})           // false т.к совпадает тип, но не значение
errors.Is(handlerErr, &httpError{})          // false т.к совпадает тип, но не значение

Выглядит логично и удобно, но в реальном мире это не будет работать. Переменные вложенных ошибок будут в другом скоупе и их невозможно будет достать чтобы сравнить. Правда можно добавить немного магии, реализовав метод Is(target error) у httpError.

func (e *httpError) Is(target error) bool {
	return reflect.ValueOf(target).Elem().Type() == reflect.TypeOf(httpError{})
}

В таком случае сработает проверка по-типу даже если значения не будут совпадать.

errors.Is(handlerErr, &httpError{})
// true

Но это хак, т.к для проверки по типу в Go добавили вторую функцию errors.As(err error, target interface{}). Она работает аналогично функции errors.Is, рекурсивно обходя матрешку, но сравнивает не значения, а тип target и вызывая .Unwrap() для продвижения вглубь. Если же совпадение найдено, но значения типа совпавшей ошибки копируется в target.

var s *rpcError
if errors.As(handlerErr, &s) {
    fmt.Printf("handlerErr as rpcError: %s\n", s.Method)
}
// handlerErr as rpcError: getEnterpriseResourceV1

var h *httpError
if errors.As(handlerErr, &h) {
    fmt.Printf("handlerErr as httpError: %d\n", h.Code)
}
// handlerErr as httpError: 500

var e error
if errors.As(handlerErr, &e) {
    fmt.Printf("handlerErr as error: %s\n", e)
}
// handlerErr as error: handler: middleware: getEnterpriseResourceV1 error: 500: Internal Server Error

var u *unknownError
if errors.As(handlerErr, &u) {
    fmt.Printf("handlerErr as unknownError: %v\n", u)
}
// не выведет ничего

У такого подхода один очевидный плюс, разработчики убили сразу двух зайцев, и сравнение по типу и распаковка ошибки. Но есть и минусы, в ситуации когда нужно просто понять что за тип ошибки нет варианта использовать конструкцию switch, как это можно сделать используя пакет pkg/errors.

В целом новый механизм работы с ошибками решает часть болей и потенциально помогает отказаться от еще одной внешней зависимости в проекте.

Luma

Нашел на /r/proceduralgeneration пост в котором некто написал фильтр для изображений, который превращает исходное изображение в условно абстрактное. Код был написан на Lua, я же решил переписать предложенный метод на Go. Во-первых я никогда не писал что-то связанное с обработкой изображений на гошке, а во-вторых почему бы и нет.

package main

import (
	"image"
	"image/color"
	"image/jpeg"
	"os"
	"sort"
)

func main() {
	file, err := os.Open("in.jpg")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	image.RegisterFormat("jpeg", "jpeg", jpeg.Decode, jpeg.DecodeConfig)

	img, _, err := image.Decode(file)
	if err != nil {
		panic(err)
	}

	rec := img.Bounds()
	cimg := image.NewRGBA(rec)

	m := make([]color.Color, rec.Dy())
	for x := 0; x < rec.Dx(); x++ {
		for y := 0; y < rec.Dy(); y++ {
			m[y] = img.At(x, y)
		}

		sort.SliceStable(m, func(i, j int) bool {
			return luma(m[i]) < luma(m[j])
		})

		for y := range m {
			cimg.Set(x, y, m[y])
		}
	}

	out, err := os.Create("out.jpg")
	if err != nil {
		panic(err)
	}

	err = jpeg.Encode(out, cimg, &jpeg.Options{Quality: 80})
	if err != nil {
		panic(err)
	}
}

func luma(c color.Color) float32 {
	r, g, b, _ := c.RGBA()

	return 0.2126*float32(r>>8) +
		0.7152*float32(g>>8) +
		0.0722*float32(b>>8)
}

TL;DR Что тут происходит? Берем пиксельную сетку изображения, делим на столбцы и в каждом из столбцов сортируем пиксели по относительной яркости.

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

Luma

Второй интересный момент момент это то, как в Go реализованы возвращаемые значения цветов. Все цвета хранятся в uint32 в диапазоне от 0 до 0xFFFF, что позволяет хранить эффективные значения для 16-битного диапазона. Как обычно, самый исчерпывающий ответ о том как это работает можно найти на SO, где же еще. Но так как формула относительной яркости работает с 8-битными значениями, то конвертируем 16-битное представление обратно в 8 бит используя битовый сдвиг вправо.

Pad - pastebin без стероидов

В то время когда к интернету подключена каждая кофеварка, sharing небольших текстовых данных между MacOS и Linux или между iPhone и Windows все еще превращается в «танцы с бубнами». Можно конечно отправить письмо самому себе, как в средневековье. А если получатель кто-то другой, то на сцену выходит Pastebin со своими непомерными требованиями к заголовкам, типу контента, уровню видимости и капчей. Можно попроще?

Встречайте Pad — сервис где не нужно ничего заполнять, выбираешь ссылку и вставляешь текст в textarea и открываешь ссылку на другом устройстве. Сервис, как сейчас принято, написан на Go, а для стораджа используется BoltDb, это такая встраиваемая key/value база данных. Завернуто это, как полагается, в Docker, куда уж без него. Исходники можно посмотреть на Github.

Книги о Биткоине и не только

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

«Эпоха криптовалют», Пол Винья

Рассуждения о природе денег, прогнозы о возможном влиянии криптовалют на экономику и пара заметок о сомнительных стартапах. Авторы «Эпохи криптовалют» – обозреватели Wall Street Journal сделали все, чтобы ни у кого не было желания дочитать книгу до конца. А отсутствие технических деталей и минимальное погружение в хронологию событий делает книгу не стоящей времени потраченного на чтение.

«Биткоин. Графический роман о криптовалюте»

Как продать комикс в котором группа мужиков гоняется за бомжом по городу? Сделать предположение что это не бомж, а предполагаемый Сатоши Накамото, мужиков поменять на наемников и агентов спецслужб. А еще нужен хакер, который будет спасать бомжа, вот 100-страничный комикс и готов. Понятно что издатели хотели хайпануть на теме криптовалют и печатают любой бред, если в названии присутствует слово Биткоин.

«Цифровое Золото», Натаниэль Поппер

Хронология событий связанных с Биткоином в период между 2008 и 2014 годами, начиная с Адама Бэка и Хэла Финни и заканчивая падением Mt.Gox и Silkroad. Истории органично переплетены, и рассказывают о ключевых личностях стоявших у истоков протокола, первых майнерах и первых инвесторах. В книге нет технических деталей но отсутствие таковых даже идет ей на пользу. Однозначно, одна из моих любимых книг в 2017 году.

«Mastering Bitcoin», Andreas M. Antonopoulos

Алгоритмы, спецификации и как протокол работает изнутри. Мало воды и много примеров кода на c++ и python. Абсолютный must read для как разработчиков, так и просто любопытных. Книга выпущена под лицензией Creative Commons и доступна для публичного скачивания на русском языке.

«Киберпреступник №1. История создателя подпольной сетевой империи», Ник Билтон

История Росса Ульбрихта и сайта Silk Road как захватывающий детектив от начала и до конца. Особенно интересно читать и паралельно изучать огромный архив документов связанных с делом Silk Road. Росс не давал интервью при написании книги, так что некоторые из его поступков описаны по косвенным признакам, а его внутренний кофликт можно списать на фантазии автора.