↑ ↑ ↓ ↓ ← → ← → B A Start

Как отказаться от RSS в 2020 году

— Я решил отказаться от RSS…

— Ок, бумер

За несколько лет получение и потребление контента изменилось. Twitter и Telegram заняли первое место среди каналов доставки информации, а RSS как был так и остался чем-то маргинальным.

В RSS меня держало несколько лент которых не было нигде, и это были не только «местечковые блоги» но информационные сайты обновления которых получать через Twitter было неудобно.

Идея трансляции RSS в Telegram занимала мои мысли давно, а беглый поиск по Github’у давал десятки готовых скриптов. Но во всех этих скриптах был фатальный недостаток - их надо было заворачивать в какие-то контейнеры, куда-то ставить, а потом еще и поддерживать. К счастью Вастрик написал лонгрид о движении NoCode после прочтения которого я понял, что время пришло. Если авторы контента не идут в Telegram, то их можно туда принести.

Первым делом зарегистрировал себе бесплатный аккаунт в Integromat, это такой IFTTT для нормальных людей. Затем создал бота в Telegram и открыл по одному каналу на каждую из RSS лент. Можно было бы все ленты доставлять в один канал, но я не пытался из мессенджера сделать RSS ридер.

Осталось собрать вместе части конструктора. Для этого я взял готовый рецепт, вставил адрес первой RSS ленты и канал доставки, а также поставил запуск каждый 15 минут. После этого клонировал получившийся сценарий меняя настройки. Через пару минут все каналы ожили и стали доставлять обновления из RSS, а я удалил все ленты из Feedly и закрыл аккаунт навсегда.

А теперь к плохим новостям. Через 5 дней Integromat радостно сообщил, что я по потратил весь бесплатный лимит. Ведь каждый сценарий при 15-минутных запусках будет тратить по ~2880 операций в месяц. Чтобы не заводить новый аккаунт заплатил за базовый план, а для настроек выбрал запуск рецептов раз в час, таким образом снизив число запусков до 720 в месяц.

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

Типичный день в энтерпрайзе чуть менее, чем полностью состоит из встреч. В офисе об этих встречах напоминает не только 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.