If you already know how to use a programming language, when learning a new language it is only natural to attempt to draw comparisons. This post is the first in a series, we'll attempt to learn Golang by re-implementing Events APIs in Go.
What are these Events
?
It'll be easier to explain this using an example, let's assume you are in a chat group with some friends. Friend #1 (who is also your roommate) buys some popcorn and posts a picture from the store, this prompts you to pick out a movie for the night and friend #2 does nothing. In this example, a message (the picture) of an event (purchase of popcorn) prompted you (a subscriber, listening for this message) to react (select movie). Friend #2 (who is also in the group) is a subscriber to the emitter (friend #1) but not to that message channel (she doesn't care about movies or popcorn).
To get further details and more examples, see Event-driven architecture wiki page.
Implementing our library
type event struct {
name string
listeners []func(args ...string)
}
func newEvent(name string) *event {
e := event{name: name}
return &e
}
First, we create the event
struct that contains the name of this event and an array of people in the group chat, in our case, functions instead of people. The newEvent
function creates and returns an event (learn about pointers) using the name supplied into it.
When this event occurs, a message is sent to all registered listeners (people in chat group) and they can react in their own way. So, we need to make it possible to send different messages that will be triggered by different events.
type EventEmitter struct {
events map[string]*event
}
For the EventEmitter
struct, we employ a map to save these events. This will help us easily retrieve events later on, we'll see this in action later. This map will contain all events we can possibly trigger. This next function finds an event if it exists in this map or creates it if it doesn't.
func (ee EventEmitter) findOrCreateEvent(event string) *event {
if _, ok := ee.events[event]; ok == false {
ee.events[event] = newEvent(event)
}
return ee.events[event]
}
Only major thing left is providing a way for registering listeners and letting these listeners know when there the event has been triggered. In registering these listeners the on
method accepts a string signifying the event and the listener we need to register (learn about variadic function).
// register new listener
func (ee EventEmitter) on(event string, listener func(args ...string)) {
e := ee.findOrCreateEvent(event)
e.listeners = append(e.listeners, listener)
}
// fire event
func (ee EventEmitter) emit(event string, args ...string) {
if e, ok := ee.events[event]; ok {
for _, listener := range e.listeners {
listener(args...)
}
}
}
The emit
method is responsible for notifying and calling all listeners for a particular event, passing all arguments from the triggered event, this is done using a loop. Note that, there is a check to ensure that this event exists before any attempt to call listeners.
See complete file.
Using the library
We'll use this library to implement a server of some sort (that actually does no real jobs presently).
type server struct {
connection string
connected bool
eventsEmitter EventEmitter
}
This server will create and maintain a connection and listeners will be triggered based on events that this server will support.
func (server server) init() {
// Register all listeners
server.eventsEmitter.on("connect", handleConnection)
server.eventsEmitter.on("connect", logMessage)
server.eventsEmitter.on("data", logMessage)
server.eventsEmitter.on("disconnect", logMessage)
}
var handleConnection = func(args ...string) {
fmt.Println(args[0])
}
var handleDisonnection = func(args ...string) {
fmt.Println(args[0])
}
var logMessage = func(args ...string) {
log.Print(args[1])
}
In this short example, connect
, data
and disconnect
are the events supported. Multiple listeners can be registered for a single event (remember we are using an array to persist them) and we can have any number of events depending on what our server does, in this case, we have only 3 events at the moment. The handleConnection
, handleDisonnection
and logMessage
functions are our actual listeners, they only log messages to the console.
The following functions are the actual events we support, within this server methods we trigger the events and by extension notify the listeners. See complete file.
// Function for connecting to server
func (server *server) connect(connectionString string) {
server.connection = connectionString
server.connected = true
server.eventsEmitter.emit("connect", server.connection, "Connected successfully")
}
// Getting data from users
func (server *server) data(userData string) {
if server.connected {
server.eventsEmitter.emit("data", server.connection, userData)
}
}
// Function for disconnecting from server
func (server *server) disconnect() {
if server.connected {
server.connected = false
server.eventsEmitter.emit("disconnect", server.connection, "Disconnected successfully")
} else {
fmt.Print("Already disconnected")
}
}
The main function initializes a server and sends some messages (assuming they are coming from different users) and then finally closes the connection.
func main() {
var s = server{
eventsEmitter: EventEmitter{
events: make(map[string]*event),
},
}
s.init()
s.connect("our/connection/string")
s.data("Message from user 1")
s.data("Message from user 2")
s.data("Message from user 3")
s.data("Message from user 4")
s.disconnect()
}
Running this program produces the output below. Showing that all listeners were triggered across all events.
http/connection/string
2021/02/19 14:29:42 Connected successfully
2021/02/19 14:29:42 Message from user 1
2021/02/19 14:29:42 Message from user 2
2021/02/19 14:29:42 Message from user 3
2021/02/19 14:29:42 Message from user 4
2021/02/19 14:29:42 Disconnected successfully
Next steps
There are several things wrong this program as it currently exists, we'll list some of them here and attempt to handle them in the near future.
- Error handling wasn't considered at all
- Listeners run synchronously, we need to be able to run them concurrently
- Use packages to correctly partition our library.