4 min read
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:
interface{}
, no need to type cast.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.
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: "[email protected]",
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: "[email protected]",
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
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: "[email protected]",
Time: time.Now(),
})
// ...
}
func DeleteUser() {
// ...
events.UserDeleted.Trigger(events.UserDeletedPayload{
Email: "[email protected]",
Time: time.Now(),
})
// ...
}
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.
In this post, we'll see how to write workers in Go, how to gracefully shut them down. We'll also look at how to coordinate multiple workers.
8 min read
There is only one keyword to perform loops in Go: for. The implementation is very flexible. In this article, we’ll consider the various ways to use...
4 min read
Comments