DEV Community

Stephen Afam-Osemene
Stephen Afam-Osemene

Posted on • Originally published at stephenafamo.com on

Implementing an Event Driven System in Go

Events in Go

In this post, we will implement an event driven system in Go.

We are going to imagine a fictional application where we want to send out events for when a new account is created and another for when an account is deleted.

Let’s assume that the current structure of our program looks like this:

working-dir
|
|__auth.go/
| |__auth.go
|
|__main.go
|__go.mod
|__go.sum
Enter fullscreen mode Exit fullscreen mode

We would like this system to:

  • Be type safe. No interface{}, no need to type cast.
  • Be able to define a custom payload for each event.

Defining Events

To make it safe, we will create our events in a separate package and only export the complete events. Let’s call this package events. We’ll update our directory structure like this.

working-dir
|
|__auth.go/
| |__auth.go
|
|__events/
|__main.go
|__go.mod
|__go.sum
Enter fullscreen mode Exit fullscreen mode

Each event would be a unique type and would define the required payload for each event. Every handler would explicitly know what data it will receive.

Here would be our event for when a user is created:

// events/user_created.go
package events

import (
    "time"
)

var UserCreated userCreated

// UserCreatedPayload is the data for when a user is created
type UserCreatedPayload struct {
    Email string
    Time  time.Time
}

type userCreated struct {
    handlers []interface{ Handle(UserCreatedPayload) }
}

// Register adds an event handler for this event
func (u *userCreated) Register(handler interface{ Handle(UserCreatedPayload) }) {
    u.handlers = append(u.handlers, handler)
}

// Trigger sends out an event with the payload
func (u userCreated) Trigger(payload UserCreatedPayload) {
    for _, handler := range u.handlers {
        go handler.Handle(payload)
    }
}

Enter fullscreen mode Exit fullscreen mode

We can then create another event for when a user is deleted:

// events/user_deleted.go
package events

import (
    "time"
)

var UserDeleted userDeleted

// UserDeletedPayload is the data for when a user is Deleted
type UserDeletedPayload struct {
    Email string
    Time  time.Time
}

type userDeleted struct {
    handlers []interface{ Handle(UserDeletedPayload) }
}

// Register adds an event handler for this event
func (u *userDeleted) Register(handler interface{ Handle(UserDeletedPayload) }) {
    u.handlers = append(u.handlers, handler)
}

// Trigger sends out an event with the payload
func (u userDeleted) Trigger(payload UserDeletedPayload) {
    for _, handler := range u.handlers {
        go handler.Handle(payload)
    }
}

Enter fullscreen mode Exit fullscreen mode

Our directory structure now looks like this:

working-dir
|
|__auth.go/
|   |__auth.go
|
|__events/
|   |__user_created.go
|   |__user_deleted.go
|
|__main.go
|__go.mod
|__go.sum
Enter fullscreen mode Exit fullscreen mode

A good thing about this system is that the event variable types are not exported, therefore, they cannot be changed or assigned to something different outside the package.

Listening for Events

To listen for an event, we import our events package and then register our handler to an event.

First, we create a listener that sends notifications to an admin and to slack when a user is created.

// create_notifier.go
package main

import (
    "time"

    "github.com/stephenafamo/demo/events"
)

func init() {
    createNotifier := userCreatedNotifier{
        adminEmail: "the.boss@example.com",
        slackHook: "https://hooks.slack.com/services/...",
    }

    events.UserCreated.Register(createNotifier)
}

type userCreatedNotifier struct{
    adminEmail string
    slackHook string
}

func (u userCreatedNotifier) notifyAdmin(email string, time time.Time) {
    // send a message to the admin that a user was created
}

func (u userCreatedNotifier) sendToSlack(email string, time time.Time) {
    // send to a slack webhook that a user was created
}

func (u userCreatedNotifier) Handle(payload events.UserCreatedPayload) {
    // Do something with our payload
    u.notifyAdmin(payload.Email, payload.Time)
    u.sendToSlack(payload.Email, payload.Time)
}
Enter fullscreen mode Exit fullscreen mode

Let’s add another listener that does the same when a user is deleted.

// delete_notifier.go
package main

import (
    "time"

    "github.com/stephenafamo/demo/events"
)

func init() {
    createNotifier := userCreatedNotifier{
        adminEmail: "the.boss@example.com",
        slackHook: "https://hooks.slack.com/services/...",
    }

    events.UserCreated.Register(createNotifier)
}

type userCreatedNotifier struct{
    adminEmail string
    slackHook string
}

func (u userCreatedNotifier) notifyAdmin(email string, time time.Time) {
    // send a message to the admin that a user was created
}

func (u userCreatedNotifier) sendToSlack(email string, time time.Time) {
    // send to a slack webhook that a user was created
}

func (u userCreatedNotifier) Handle(payload events.UserCreatedPayload) {
    // Do something with our payload
    u.notifyAdmin(payload.Email, payload.Time)
    u.sendToSlack(payload.Email, payload.Time)
}
Enter fullscreen mode Exit fullscreen mode

Now, we will have a directory structure that looks like this:

working-dir
|
|__auth.go/
|   |__auth.go
|
|__events/
|   |__user_created.go
|   |__user_deleted.go
|
|__create_notifier.go
|__delete_notifier.go
|__main.go
|__go.mod
|__go.sum
Enter fullscreen mode Exit fullscreen mode

Triggering Events

Now that we have our listeners set up, we can then trigger these events from our auth package (or anywhere else).

// auth.go
package auth

import (
    "time"

    "github.com/stephenafamo/demo/events"
    // Other imported packages
)

func CreateUser() {
    // ...
    events.UserCreated.Trigger(events.UserCreatedPayload{
        Email: "new.user@example.com",
        Time: time.Now(),
    })
    // ...
}

func DeleteUser() {
    // ...
    events.UserDeleted.Trigger(events.UserDeletedPayload{
        Email: "deleted.user@example.com",
        Time: time.Now(),
    })
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

We saw a way to define events in a type safe manner, how to listen for these events and how to trigger them.

Nothing fancy. As with all things Go, a good solution is a boring solution.

The post Implementing an Event Driven System in Go appeared first on Stephen AfamO's Blog.

Discussion (2)

Collapse
firas profile image
Firas M. Darwish

that was really helpful, thanks!