Part 2: Go Meets Different Patterns

As mentioned in the first part of the blog, I am a lifetime Java developer switching to Go programming language to implement a simple container-based K8s API client. Exploring Go features was fun, and I decided to implement a simple message processor.
And what’s the best way to explore it but to use some of the most common design patterns implemented with the Go standard library?
In the previous blog, I covered Singleton and Publish-subscribe. In this one, I’ll cover the implementation of post-factory and command patterns.
Factory pattern
Factory pattern is a creational pattern and more important one of the most commonly used ones.
Using the factory pattern, objects are created without exposing the creation logic to the client. This refers to newly created objects using their common interfaces. For this, we use JSON marshaling and unmarshalling.
Built-in JSON serialization and deserialization
As a modern programming language, Go has first-class support for JSON serialization in its standard library.
It uses the Go json package and its underlying Marshal and Unmarshal functions to serialize and deserialize byte slices to structs or simple types (e.g. maps) and vice versa.
From JSON to Objects
As mentioned earlier, there is a JSON from the HTTP request that is sent down the event bus and received for processing on inputChannel
.
This is where the transformation of JSON into a meaningful Go type happens. One could say – to use the json package and Unmarshal whatever is in JSON to a structure. That would be the simplest way to put it.
However, in this case, JSON contains a field that indicates a type or kind of payload.
In general, there can be a few different payloads that are unmarshaled to different types, as you will see below.
Different payloads
{"kind": "Temperature", "value": -20}
{"kind": "Humidity", "value": 78}
{"kind": "Wind", "gust": 10, "lull": 10, "direction": 10}
{"kind": "Lightning", "power": 1000}
All of the payloads contain a kind
field that indicates the corresponding type of event. So, straightforward unmarshaling won’t work in this case.
Factory pattern gives a simple approach to tackling this.
Using the
kind
field as a discriminant for input parameter in the creation method. It then creates a corresponding structure and returns a reference to it as a type of interface.
First, take a look at the structures which represent each of the above payloads.
type TopicHolder struct {
NotificationTopics []string
AlertTopics []string
}
type TemperatureReading struct {
TopicHolder
Reading float32 `json:"value"`
}
type HumidityReading struct {
TopicHolder
Reading float32 `json:"value"`
}
type WindReading struct {
TopicHolder
Gust int32 `json:"gust"`
Lull int32 `json:"lull"`
Direction float32 `json:"direction"`
}
type LightningReading struct {
TopicHolder
power int32 `json:"power"`
}
Notice that these structures do not have a field for mapping the kind since they represent the type of event with their definition.
The factory itself uses a kind
field to spawn new events. What does the factory look like, then?
Simply put, the factory is just a method CreateReading on a WeatherEventFactory struct.
Methods in Go, unlike normal functions, are defined using the so-called receiver type which is indicated in front of the method name.
Take a look at the code below.
type WeatherEventFactory struct {
NotificationTopics []string
AlertTopics []string
}
func (factory WeatherEventFactory) CreateReading(rawEvent []byte) (Executor, error) {
var eventWrapper Wrapper
if err := deser(rawEvent, &eventWrapper); err != nil {
return nil, err
}
if spawnEvent, present := eventHatchery[eventWrapper.EventType]; present {
fmt.Printf("Building event [%s]\n", eventWrapper.EventType)
e := spawnEvent(factory.NotificationTopics, factory.AlertTopics)
if err := populate(rawEvent, e); err != nil {
return nil, err
}
return e, nil
} else {
return nil, fmt.Errorf("unknown event type [%s]", eventWrapper.EventType)
}
}
The CreateReading
the method has a receiver type of factory struct and a byte slice parameter that contains the whole JSON payload. It returns either an interface Executor or an error.
JSON slice is deserialized into a wrapper type which contains just a kind
field mapping variable, the EventType
.
type Type string
type Wrapper struct {
EventType Type `json:"kind"`
}
A reference of the wrapper is sent to the deser
function to use it with json.Unmarshal
.
And deserialization is simple as this:
func deser(rawEvent []byte, eventWrapper *Wrapper) error {
err := json.Unmarshal(rawEvent, eventWrapper)
if err != nil {
return err
}
return nil
}
Next, in the CreateReading
method, we use a deserialized type from the wrapper and call the function for spawning a new Executor.
Executors are created from functions defined inside of the eventHatchery
map. Each type of event has its own creational function in this map:
var eventHatchery = map[Type]func(notify []string, alert []string) Executor{
Temperature: func(notificationTopics []string, alertTopics []string) Executor {
return &TemperatureReading{TopicHolder: TopicHolder{NotificationTopics: notificationTopics, AlertTopics: alertTopics}}
},
Humidity: func(topics []string, _ []string) Executor {
return &HumidityReading{TopicHolder: TopicHolder{NotificationTopics: topics}}
},
Wind: func(notificationTopics []string, alertTopics []string) Executor {
return &WindReading{TopicHolder: TopicHolder{NotificationTopics: notificationTopics, AlertTopics: alertTopics}}
},
Lightning: func(notificationTopics []string, alertTopics []string) Executor {
return &LightningReading{TopicHolder: TopicHolder{NotificationTopics: notificationTopics, AlertTopics: alertTopics}}
},
}
Finally, a call to the creation function for the event and a call to the populate
method which uses the original byte slice and unmarshals the rest of the JSON payload to an actual type, similar to how the wrapper was unmarshalled.
unc populate(rawEvent []byte, event Executor) error {
err := json.Unmarshal(rawEvent, event)
if err != nil {
return err
}
return nil
}
Command pattern
At last, a command pattern, a behavioral pattern where an object (command) encapsulates all parameters to perform some action.
In this way implementation of actual action is hidden from the caller, and the action and implementation of the caller are decoupled.
Interface
Interfaces in Go programming language are at the same time type and, more importantly, a set of methods assigned to a type. Maybe it is harder to explain it with words than with a good example.
For a nice and simple example, look at the below implementation of the command pattern.
What does this type do?
Back to the code example, JSON is now a Go type of:
- TemperatureReading,
- HumidityReading,
- WindReading,
- LightningReading.
Naturally, for each of the readings, a notification is sent to the EventBus
, along with an additional Alert – if the readings’ value is alert-worthy.
Action logic is implemented with an Execute
method defined inside of the Executor
interface. Each of the above readings has that method associated with using receiver argument like the one shown here:
type Executor interface {
Execute() error
}
func (reading TemperatureReading) Execute() error {
...
}
func (reading HumidityReading) Execute() error {
...
}
func (reading WindReading) Execute() error {
...
}
func (reading LightningReading) Execute() error {
...
}
Each of the methods has its own implementation on how the reading data triggers either notification or alert sending. In case of a failure, the method simply returns an error leaving it to the invoker to handle it.
Looking at the example of WindReading
the logic, we can see that we decide if the wind gust is strong enough to send critical, high, or there is no need for an alert at all. Alerts and notifications are sent using the already explained mechanism of the publish-subscribe pattern through the event bus.
Notice the use of reading instance, it is defined as a so-called receiving argument. It provides an instance of the struct on which the call is made. We check the reading parameters and use the injected topics used for alert and notification sending. And the call of Execute
function on a structure instance looks like this:
if err := e.Execute(); err != nil {
fmt.Printf("Error executing event: %s\n", err)
}
That’s it; we do the error handling there. Imagine there could be a good use of the event bus for error handling and processing. The whole process function used for processing the event came from the input channel with its JSON payload to the sending notification or alert based on the reading. That would be the following:
func Process(event bus.Event) {
factory := command.GetFactoryInstance()
reading, err := factory.CreateReading([]byte(event.Data.(string)))
if err != nil {
fmt.Printf("Error deserializing event `%s`: %s\n", event.Data, err)
return
}
if err := reading.Execute(); err != nil {
fmt.Printf("Error executing event: %s\n", err)
}
}
In other words, simple as fetching the factory instance, deserializing JSON payload using the factory, and executing the reading action on the deserialized reading.
Quick project for a quick conclusion
As you can see in the example project, Go is really simple to learn and use language. There are so many cool features and things you can implement that require so little coding just by using Go programming language or its standard library features. Not to mention that it’s lightweight and quick because of the runtime being part of each binary.
On the other hand, coming from the world of Java where I have used Spring Framework to a great extent in the past 16 years, and inversion of control and dependency injection. Do I miss it? Of course. If you are developing something a bit bigger or more robust and testable, you need it.
Fortunately, there are few modules to answer these needs and I guess the best I saw is Google’s Wire.
Overall, if you are developing something from scratch and it needs to be spun up on the cloud or in a container this is a way to Go.