Колокол — система событий в Go или очередная event-system библиотека

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

В работе над одним проектом в компании NUT.Tech нам понадобилась система событий, работа которой не влияла бы на основной поток выполнения программы. 

Требования к системе были довольно простыми:

  • Возможность подписываться на события,

  • Возможность уведомлять систему о событии,

  • Возможность передавать в обработчики событий дополнительную информацию,

  • Простая реализация обработчиков событий,

  • Выполнение обработчиков событий не должно никак аффектить основной поток программы.

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

  • https://github.com/ReactiveX/RxGo - популярная библиотека, обладающая нужной нам функциональностью, но эта функциональность только малая часть того, что это библиотека умеет. А нам очень не хотелось использовать что-то большое ради одной небольшой функции. Это похоже на забивание гвоздей микроскопом.

  • https://github.com/gookit/event - показалась нам переусложненной.

  • https://github.com/agoalofalife/event - не умеет запускать обработчики в отдельном потоке.

  • https://github.com/AlexanderGrom/go-event - также не умеет запускать обработчики в отдельном потоке. Зато нам понравилась легковесность библиотеки и простой интерфейс.

Остальные найденные нами библиотеки были или очень объемные, с большим количеством настроек (а нам хотелось что-то простое и легкое), либо давно не обновлялись, либо работали в том же потоке, что и основная программа.

В общем, так и не сумев найти отвечающую всем нашим требованиям библиотеку, мы решили, что проще и быстрее будет написать все самим (какая редкость для Golang, да?).

Сначала мы добавили нашу систему событий как часть основного приложения, но с требованием “в будущем должно быть легко выделить код в отдельный пакет”. На написание непосредственно кода и тестов к нему ушло несколько дней.

Ниже расскажу и покажу на примерах, что у нас получилось.

Библиотека написана на языке Go и представляет из себя простейшую систему событий, которая основана на выполнении обработчиков независимо от основного потока.

Особенностями библиотеки являются:

  • Нет зависимостей от сторонних библиотек,

  • Возможность добавить несколько обработчиков одного или нескольких событий,

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

  • Возможность передавать любые пользовательские данные в обработчики событий,

  • Полное покрытие тестами.

Исходный код и примеры можно посмотреть по ссылке https://github.com/nuttech/bell.

Использование библиотеки

Для добавления пакета в приложение достаточно выполнить команду

go get -u github.com/nuttech/bell

и далее в нужном файле проимпортировать ее:

import "github.com/nuttech/bell"

Чтобы добавить обработчик того или иного события, вам нужно добавить следующий код:

bell.Listen("event_name", func(message bell.Message) {
    // здесь код вашего обработчика
})

Первый аргумент функции - это название события, им может быть любая строка. Например, для вызова события успешной авторизации пользователя можно использовать название “user_login_success”.

Второй аргумент функции Listen - это функция, которая на вход принимает структуру bell.Message. Это и есть ваша функция-обработчик события. В структуре bell.Message будет передана системная информация и пользовательские данные:

type Message struct {
	Event     string // название события
	Timestamp time.Time // время вызова события
	Value     interface{} // пользовательские данные
}

Так как Message.Value - это interface{}, туда можно передать что угодно: идентификатор, строку, структуру и т.д.

Обработчиков события можно добавлять сколько угодно много. Все они будут вызваны в отдельной горутине:

bell.Listen("event_name", func(message bell.Message) { 
	// первый обработчик
})

bell.Listen("event_name", func(message bell.Message) {
	// второй обработчик
})

Для того, чтобы вызвать событие и запустить обработчики, достаточно добавить примерно такой код:

bell.Call("event_name", "some data")

где первый параметр - это название события, а второй - пользовательские данные, которые вы захотите передать в обработчики.

Например, если у вас есть структура userStruct и вызов события выглядит следующим образом:

type userStruct struct {
	Name string
}

bell.Call("event_name", userStruct{Name: "Jon"})

То обработчик может может быть таким:

bell.Listen("event_name", func(message bell.Message) {
	user := message.Value.(userStruct)
	fmt.Printf("%#v\n", userStruct{Name: "Jon"})  // main.userStruct{Name:"Jon"}
})

Вспомогательные функции

Пакет также содержит еще несколько вспомогательных функций:

bell.List()

Функция вернет список всех событий, на которые добавлены подписчики.

bell.Has("event_name")

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

_ = bell.Remove()

А эта функция удалит все обработчики всех событий.

_ = bell.Remove("event_name")

Если передать в функцию bell.Remove название события, то будут удалены только обработчики переданного события.

Полный пример работы пакета

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

Пример использования библиотеки Bell
package main

import (
  "fmt"
  "github.com/nuttech/bell"
  "log"
  "net/http"
  "net/url"
  "time"
)

const requestEvent = "request"
const loginSuccessEvent = "login_success"

type LoginRequest struct {
  Method    string
  Path      string
  UserAgent string

}

type User struct {
  Login string
}

func main() {
  // создаем обработчик события request, который будет выводить информацию о запросе
  bell.Listen(requestEvent, func(message bell.Message) {
     time.Sleep(time.Second * 2)
     r := message.Value.(LoginRequest)
     log.Printf("%s %s, %s", r.Method, r.Path, r.UserAgent)

  })

  // Создаем два обработчика события успешного логина
  // Первый будет писать локальный лог
  bell.Listen(loginSuccessEvent, func(message bell.Message) {
     data := message.Value.(User)
     log.Printf("%#v\n", data)
  })

  // Второй будет отправлять данные на какой-то сторонний сервис
  bell.Listen(loginSuccessEvent, func(message bell.Message) {
     userData := message.Value.(User)
     rData := url.Values{
        "login": {userData.Login},
     }

     // шлем запрос локально для упрощения примера
     ,  = http.PostForm("http://localhost:8888/log", rData)

  })

  // Создадим обработчик запроса на запись лога
  http.HandleFunc("/log", func(writer http.ResponseWriter, request *http.Request) {
     log.Println("Save login request")
     request.ParseForm()
     fmt.Printf("%#v\n", request.PostForm)
  })

  http.HandleFunc("/login", func(writer http.ResponseWriter, request *http.Request) {
     r := LoginRequest{
        Path:      request.RequestURI,
        Method:    request.Method,
        UserAgent: request.UserAgent(),
     }

     // Вызываем событие request и продолжаем работу обработчика
     _ = bell.Ring(requestEvent, r)

     // получаем логи и пароль
     request.ParseForm()
     login := request.FormValue("login")
     pass := request.FormValue("password")

     if login != "login" || pass != "pass" {
        writer.WriteHeader(http.StatusUnauthorized)
        return
     }

     // вызываем событие успешного логина
     _ = bell.Ring(loginSuccessEvent, User{Login: login})
     // и сразу отдаем клиенту 200 ответ
     writer.WriteHeader(http.StatusOK)
  })

  log.Fatal(http.ListenAndServe(":8888", nil))
}

Заключение

В итоге, у нас получилась легкая и простая библиотека, которая отвечает всем нашим требованиям. Теперь она используется в наших проектах. 

Мы будем рады любым замечаниям и предложениям по доработке данной библиотеки.

Источник: https://habr.com/ru/post/651453/


Интересные статьи

Интересные статьи

На прошлой неделе издание DarkReading опубликовало показательный кейс о безопасности промышленных IT-систем. К сожалению, ни в статье издания, ни в оригинальном посте исследователей почти не приводитс...
Привет, Хабр! В этом году мы провели пятый ежегодный опрос о состоянии экосистемы разработки и выяснили, чем живут программисты, какие языки, технологии и инструменты они используют и как изменился ми...
Представьте, что у вас полная серверная инженерного оборудования: несколько десятков кондиционеров, куча ДГУ и бесперебойников. Чтобы «железо» работало как надо, вы регулярно проверяе...
Помните историю с утечкой паспортных данных у 500 млн клиентов сети отелей Marriott? Данные могли оказаться у злоумышленников, и гостиничная группа даже обещала оплатить пострадавшим постояльцам ...
В классическом понимании дуговая защита в России – это быстродействующая защита от коротких замыканий, основанная на регистрации спектра света открытой электрической дуги в КРУ, наиболее расп...