DEV Community

loading...
Cover image for Handler and Middleware design pattern in Golang

Handler and Middleware design pattern in Golang

Alexandre Couedelo
I embrace the DevOps culture since I started my career by contributing to the digital transformation of a leading financial institution in Canada.
Originally published at Medium ・5 min read

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.

Middleware explained with sequence diagram

  1. A Server that listens to Events and dispatches them to the proper handler(s) function.
  2. One or many middlewares that perform pre et/or post-processing of the event
  3. 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)
})
Enter fullscreen mode Exit fullscreen mode
eventHandler.Handle(event1, handler)
Enter fullscreen mode Exit fullscreen mode

All handler functions have the same signature; for instance, in net/http the look like this:

func(w http.ResponseWriter, r *http.Request)
Enter fullscreen mode Exit fullscreen mode

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
        }
  )
}
Enter fullscreen mode Exit fullscreen mode
eventHandler.Handle(event1, ExampleMiddleware(handler))
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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"
)
Enter fullscreen mode Exit fullscreen mode

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{}
}
Enter fullscreen mode Exit fullscreen mode

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)
    }

}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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,
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Using our system

Everything is ready, we can start using our event handling system.

  1. Instantiate our event Handler
  2. Register which event to listen to and what function to callback
  3. Start the event sender
  4. Start the event dispatcher
func main() {

    eventHandler := NewEventHandler()

    eventHandler.Handle(event1, func() {
        log.Printf("event Handled: %v", event1)
    })

    go eventSender(eventHandler.Events)

    eventHandler.EventDispatcher()

}
Enter fullscreen mode Exit fullscreen mode

The result should be along those lines:

Example code run in the console

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
Enter fullscreen mode Exit fullscreen mode

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.

Interesting Articles tackling the same topic

Discussion (0)