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
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
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)
}
}
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)
}
}
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
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)
}
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)
}
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
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(),
})
// ...
}
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.
Top comments (1)
that was really helpful, thanks!