В работе над одним проектом в компании 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)) }
Заключение
В итоге, у нас получилась легкая и простая библиотека, которая отвечает всем нашим требованиям. Теперь она используется в наших проектах.
Мы будем рады любым замечаниям и предложениям по доработке данной библиотеки.
