Колокол — система событий в 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/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *