Exploring Design Patterns in Go
Design patterns are reusable solutions to common software design problems that help developers build software that is maintainable, extensible, and scalable. In this article, we will explore some popular design patterns in Go: Builder, Decorator, Factory Method, Fan-in-out, and Singleton.
1. Builder Pattern
The Builder pattern separates the construction of a complex object from its representation, allowing the same construction process to create different representations. It provides a step-by-step approach to building objects. Let’s see an example of the Builder pattern in Go:
In the above example, UserBuilder provides methods to set different attributes of the user and a Build method to create the final User object.
2. Decorator Pattern
The Decorator pattern allows you to wrap existing functionality and append or prepend your own custom functionality on top. It enables adding behavior to objects dynamically at runtime. Let’s see an example of the Decorator pattern in Go:
In this example, we have a mainFun function representing the main functionality, and the additionalFun function acts as a decorator, adding additional behavior before and after executing the mainFun function by accepting it as an argument.
3. Factory Method
The Factory Method pattern allows the creation of objects without specifying their exact types and delegates the instantiation to the factory. It provides a way to decouple the abstraction and implementation of object creation and enables flexibility in creating different types of objects.
In this example, we have a factory method called GetEngine that returns Car or Train objects based on the specified engineType. The main function showcases the usage of the factory method by creating engine objects and invoking their Start() and Stop() methods.
4. Fan-In and Fan-Out Patterns
- Fan-In Pattern
In the Fan-In pattern, multiple input channels are combined into a single output channel. The inputs can come from different sources, and the Fan-In pattern allows you to merge and process the data from these sources concurrently. It aggregates data from multiple channels into a single channel, enabling centralized processing. Here’s an example:
In this Fan-In example, we have a generator
function that returns a channel that produces numbers in a specified range.
We have multiple squareWorker
goroutines that receive numbers from the numbers
channel, square each number, and send the squared result to the squaredNumbers
channel. Each worker goroutine is synchronized using a sync.WaitGroup
.
The main goroutine uses the generator
function to create a numbers
channel with numbers from 1 to 5. It then launches multiple squareWorker
goroutines to process the numbers concurrently.
The main goroutine waits for the workers to finish processing by calling wg.Wait()
. It then closes the squaredNumbers
channel and receives the squared numbers from the channel, printing them.
2. Fan-out Pattern
In the Fan-Out pattern, a single input channel is divided and distributed among multiple worker goroutines. Each worker receives a portion of the workload and operates on it independently. The Fan-Out pattern allows you to parallelize the processing of data by dividing the workload and assigning it to multiple workers.
In this Fan-Out example, we have a producer
goroutine that sends numbers from 1 to 5 to the input
channel and then close it.
We also have multiple worker
goroutines that receive numbers from the input
channel, perform some processing (in this case, multiply the number by 2), and send the result to the output
channel. Each worker is synchronized using a sync.WaitGroup
.
The main goroutine waits for the workers to finish processing by calling wg.Wait()
. It then closes the output
channel and receives the processed results from the output
channel, printing them.
5. Singelton Pattern
The Singleton pattern ensures that only one instance of a class is created throughout the application. It provides a global point of access to this instance, allowing shared access to its resources. Let’s explore different implementations of the Singleton pattern in Go:
1. Not Thread Safe (NTS)
The first implementation we’ll look at is the Non-Thread Safe (NTS) approach. While simple, it’s not suitable for concurrent scenarios:
In this implementation, the GetSingletonNTS
function lazily initializes the instance
variable. However, if multiple goroutines access GetSingletonNTS
simultaneously and instance
is still nil
, they may end up creating separate instances.
2. Mutex Lock
To introduce thread safety, we can use a sync.Mutex
to control access to the initialization code:
In this implementation, we use a sync.Mutex
named lock
to synchronize access to the instance
initialization. While it ensures thread safety, it introduces a potential bottleneck when multiple goroutines need to acquire the lock.
3. Check-Lock-Check
We can improve performance by minimizing the number of lock acquisitions using a double-checked locking approach:
In this implementation, we first perform a quick check on instance
without acquiring the lock. If instance
is still nil
, we acquire the lock and perform a second check before creating the instance. This approach reduces the number of lock acquisitions but is prone to subtle bugs in certain scenarios.
4. Using sync.Once
The recommended way to implement the Singleton pattern in Go is to use the sync.Once
package's Do
function:
In this implementation, the GetSingletonOnceDo
function ensures that the initialization code inside the once.Do
block is executed only once, regardless of the number of calls to GetSingletonOnceDo
. It provides thread safety and better performance compared to the other implementations.
The complete codebase for the examples discussed in this article is available on the Git repository, allowing you to easily access and experiment with the entire codebase.