I recently started working on a SlackBot project of my own using Golang. As any good coder would, I went on a quest for a library that would simplify my day and let me take advantage of what other community members have come up with to solve similar problems.
I came across the well-maintained slack-go library; I started coding my bot using the provided example. Everything worked fine; The code is producing the expected result. It is time to make another coffee and implements a few extra features.
Back at my desk, plunging myself bask into my code, something strikes me. Wait a minute! This working way does not feel quite right, though. When coding REST APIs, I do not have to write any complex logic to handle my event because net/http
is designed to help me handle HTTP requests. How can I leverage that design pattern to simplify my life and make my code more clean and flexible? I concluded.
In general, when using third-party libraries, always ask yourself if there is no better way to work and learn several design patterns. I will make you significantly more efficient by preventing you from reinventing the wheel.
Any library providing event parsing should also adopt a Handler, Middleware approach; It would feel natural for most programmers because it is the foundation of most APIs.
Overview
The Handler and Middleware design pattern (also called Chain of Responsibility) allows multiple functions to handle an event by decoupling sender and receiver(s). The chain can be composed of any amount of handler that follows a standard signature or interface.
- A Server that listens to Events and dispatches them to the proper handler(s) function.
- One or many middlewares that perform pre et/or post-processing of the event
- A handler function that processes the event
handler function
A handler or callback is a function invoked to handle a particular type of event. In Golang, it can be an anonymous function or a regular function.
eventHandler.Handle(event1, func(e Event) {
log.Printf("event Handled: %v", event1)
})
eventHandler.Handle(event1, handler)
All handler functions have the same signature; for instance, in net/http
the look like this:
func(w http.ResponseWriter, r *http.Request)
Middleware
A middleware handler is simply a function that wraps another handler. In other words, it's called a "middleware" because it sits in the middle between the server and the handler. This is how you would implement any middleware in Go:
func ExampleMiddleware(next Handler) Handler {
return Handler(
func(e Event) {
//Pre-processing goes there
// The actual handler
next(e Event)
//Post-processing goes there
}
)
}
eventHandler.Handle(event1, ExampleMiddleware(handler))
A middleware always takes as input at list one handler function and returns a handler function. This offers the most flexibility and lets you chain middlewares. For instance, you can do that:
eventHandler.Handle(event1, ExampleMiddlewareFoo(ExampleMiddlewareBar(handle)))
Use Case: Implementing your Handlers, Middleware system
Designing events
First, we are going to enumerate the list of events that our system can handle. The way we create enumeration in Go is a bit different than in other programming languages. In Go, we are going to use a set of constants sharing the same type
. Here I called it EventType
, and it represents a string with the event's name.
// type used to enumerate events
type EventType string
const (
event1 EventType = "event1"
event2 EventType = "event2"
)
Next, we define the event itself. In our example, our Event
as a type among the list of EventType
created above and arbitrary data.
type Event struct {
Type EventType
Data interface{}
}
Create an eventSender (for test purpose)
To test our system, we will need to create a small function to send events every 2s. Event
are transmitted via a channel. The eventSender
function below sends a random Event
of type event1
or event2
to a channel.
Channels are a type of thought which you can send and receive values, are great for communication among goroutines. In other words, there are perfect for sending and receive event thought your application.
func eventSender(c chan EventType) {
for {
// Send a random event to the channel
rand.Seed(time.Now().Unix())
events := []EventType{
event1,
event2,
}
n := rand.Int() % len(events)
c <- events[n] // send event to channel
// wait a bit
time.Sleep(2 * time.Second)
}
}
Handler and dispatcher
We first need a struct to hold the list of events we want to listen to and which function to call whenever that event is transmitted. This struct also contains the channel used for communicating events.
// Create a struct to hold config
// And simplify dependency injections
type EventHandler struct {
// Event channel
Events chan Event
// hold the registedred event functionss
EventMap map[EventType][]func(Event)
}
Next, we need to provide an initializing constructor for our EventHandler
.
func NewEventHandler() *EventHandler {
eventMap := make(map[EventType][]func(Event))
events := make(chan Event)
return &EventHandler{
Events: events,
EventMap: eventMap,
}
}
Then, we can create our Handle
function that register the *event to listen and the callback function.
// register the handler function to handle an event type
func (h *EventHandler) Handle(e EventType, f func(Event)) {
h.EventMap[e] = append(h.EventMap[e], f)
}
Finally, we create the EventDispatcher
function, the core of this mechanism. The function process any event sent to a channel. When an event arrives, we check the type of the event. If any function has been registering for that eventType
, we call all registered function for that eventType
func (h *EventHandler) EventDispatcher() {
for evt := range h.Events {
log.Printf("event recieved: %v", evt)
if handlers, ok := h.EventMap[evt.Type]; ok {
// If we registered an event
for _, f := range handlers {
// exacute function as goroutine
go f(evt)
}
}
}
}
Using our system
Everything is ready, we can start using our event handling system.
- Instantiate our event Handler
- Register which event to listen to and what function to callback
- Start the event sender
- Start the event dispatcher
func main() {
eventHandler := NewEventHandler()
eventHandler.Handle(event1, func() {
log.Printf("event Handled: %v", event1)
})
go eventSender(eventHandler.Events)
eventHandler.EventDispatcher()
}
The result should be along those lines:
Since we only handle an event of type event1
, only event1
shows as Handled
. All is good!
Try this example
Once you have successfully completed the tutorial, you can run your app:
go run ./main.go
You can also directly clone my repository to try it beforehand. This repository contains a SlackBot demo that uses this design pattern. The specific example for this article is in examples/middleware.
Top comments (0)