Exploring Design Patterns in Go

Kamna Garg
4 min readMay 17, 2023

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

  1. 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.

--

--

Kamna Garg

Software Developer, Women in tech, Seeker, Love writing, Always a student, IIT Kanpur