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