DEV Community

Cover image for How to implement Singleton design pattern in Go ? 🔥
Rahul Yarragodula
Rahul Yarragodula

Posted on

5 3

How to implement Singleton design pattern in Go ? 🔥

Introduction

Before diving deep into the actual topic, it’s really important to make yourself familiar with the terminologies below.

What are Design patterns ?

In software engineering design patterns are solutions to general problems that occurred during the development of a software application faced by the majority of developers. The solutions were standardized over a period of trial and errors.

What is Race condition ?

A race condition is a scenario of a program where its behavior depends on relative timing or interleaving of multiple threads or processes. One or more possible outcomes may be undesirable, resulting in a bug.

What is the Singleton Design Pattern?

Singleton pattern restricts instantiation of a class or struct to only one single instance at any point of time. Usually its scope is defined as global to allow access of this instance across your application. This design pattern comes really handy in use cases such as logging, caching, Thread pool, DB connections,etc.

Singleton pattern could be implemented in one of 2 ways i.e Thread Safe and Non Thread Safe. By the way it's suggested to use thread safe.

What is Thread safe?

Thread safety is a mechanism to protect your application from entering into Race condition if it is having a shared state being manipulated by multiple threads at the same time.

What is Non Thread safe?

It does not check the safety of the threads which makes it faster to run but at the same time, it becomes more unstable for multithreaded applications. It works best for either single thread or sequential applications.

First let’s see the Non Thread Safety in action

Non Thread safe implementation in Go

As you can see in the code snippet below, a struct named SQLConnection and 3 functions named mockConnectionNonThreadSafe, performConcurrentAction, performSequentialAction are being implemented.

package main
import (
"log"
"sync"
"time"
)
// This is struct for mocking the connection.
type SQLConnection struct {
connectionUrl string
// add other details as needed
}
// This is a variable declaration in Go.
var sqlInstance *SQLConnection
func mockConnectionNonThreadSafe(threadId int) {
if sqlInstance == nil {
// This is a blocking call to mimic the time it takes
// to create a connection in real world
time.Sleep(time.Second)
// This is a variable assignment in Go.
sqlInstance = &SQLConnection{
connectionUrl: "some connection object",
}
log.Println("Created connection by thread id:", threadId)
}
}
func performConcurrentAction() {
// This is essentially needed for waiting for the program
// to finish its concurrent tasks before exiting the program in Go.
var wg sync.WaitGroup
// iterate over 10 times
// and call the mockConnectionNonThreadSafe function
// concurrently
for i := 0; i < 10; i++ {
// add 1 to the wait group
wg.Add(1)
go func(threadId int) {
// defer is used to ensure that the wait group is
// decremented after the goroutine completes
// this is done to ensure that the program doesn't
// exit before all the goroutines complete
defer wg.Done()
log.Println("thread id:", threadId)
mockConnectionNonThreadSafe(threadId)
}(i)
}
// wait for all the goroutines to complete
wg.Wait()
}
func performSequentialAction() {
// iterate over 10 times
// and call the mockConnectionNonThreadSafe function
for i := 0; i < 10; i++ {
mockConnectionNonThreadSafe(i)
}
}
// main is the entry point for the application.
func main() {
// below function call is for concurrent execution
// performConcurrentAction()
// below function call is used to execute the sequential action
performSequentialAction()
}

In the mockConnectionNonThreadSafe function, a sleep command is added to just mimic the real world scenario( creation of connection object always takes some arbitrary time).

To see non thread safe sequential execution in action, uncomment the performSequentialAction block in main function and run the snippet using this command.

go run singleton_pattern/non_thread_safe.go
Enter fullscreen mode Exit fullscreen mode

non-thread-safe-sequential.png

If you get the same output as above, then you have successfully implemented the non thread safe sequential program. The output clearly shows that sequential execution works absolutely fine, in fact it’s just created a single instance which is kind of a singleton pattern, but performance is a trade off here because this is a step by step execution which means this program can’t take advantage of multiple cores and threads of a modern day cpu.

To test non thread safe concurrent execution, uncomment the performConcurrentAction block in main function and run the snippet.
The output will look something similar to this.

non-thread-safe-concurrent.png

If you watch the output closely, you will find that the database instance is continuously modified by every concurrent execution due to some delay in creation of a connection instance ( which is expected in the real-world may be due to network or overload or something else). This is happening because the provided solution is not thread safe, that’s why the connection object is modified by other threads kind of a race condition. In this case we have achieved to successfully utilize the full cpu performance i.e cores and threads, but it’s still far away from the desired output.

Now it’s finally time to resolve this once for all.

Thread Safe implementation in Go

Again mostly similar code 1 struct and 3 functions(1 being mockConnectionThreadSafe) has been implemented.
As well as a new variable once of type sync.Once(Once is a Golang thread safe built-in struct that is used to implement a singleton pattern) has been added. This variable has a method Once which will handle thread safe execution.

package main
import (
"log"
"sync"
"time"
)
// This is struct for mocking the connection.
type SQLConnection struct {
connectionUrl string
// add other details as needed
}
// There are two ways to implement singleton pattern in Go
// i.e using sync.Once and using sync.Mutex.
// Once is a Go built-in function that is used to run a function only once.
// Once is suggested to use over Mutex due to its performance.
var once sync.Once
// This is a variable declaration in Go.
var sqlInstance *SQLConnection
func mockConnectionThreadSafe(threadId int) {
once.Do(func() {
// This is a blocking call to mimic the time it takes
// to create a connection in real world
time.Sleep(time.Second)
// This is a variable assignment in Go.
sqlInstance = &SQLConnection{
connectionUrl: "some connection object",
}
log.Println("Created connection by thread id:", threadId)
})
}
func performConcurrentAction() {
// This is essentially needed for waiting for the program
// to finish its concurrent tasks before exiting the program in Go.
var wg sync.WaitGroup
// iterate over 10 times
// and call the mockConnectionThreadSafe function
for i := 0; i < 10; i++ {
// add 1 to the wait group
wg.Add(1)
go func(threadId int) {
// defer is used to ensure that the wait group is
// decremented after the goroutine completes
// this is done to ensure that the program doesn't
// exit before all the goroutines complete
defer wg.Done()
log.Println("thread id:", threadId)
// This is a blocking call to mimic the time it takes
mockConnectionThreadSafe(threadId)
}(i)
}
// wait for all the goroutines to complete
wg.Wait()
}
func performSequentialAction() {
// iterate over 10 times
// and call the mockConnectionThreadSafe function
for i := 0; i < 10; i++ {
mockConnectionThreadSafe(i)
}
}
// main is the entry point for the application.
func main() {
// This is a call for the concurrent execution of the
// mockConnectionThreadSafe function.
performConcurrentAction()
// This is a call for the sequential execution of the
// mockConnectionThreadSafe function.
// performSequentialAction()
}
view raw thread_safe.go hosted with ❤ by GitHub

Execute this snippet by uncommenting performConcurrentAction block in main function.

go run singleton_pattern/thread_safe.go
Enter fullscreen mode Exit fullscreen mode

thread-safe-concurrent.png

If you have done everything correctly till now, then you will see the database instance being created only once( unlike connection objects being modified by other concurrent executions in case of non thread safe) even though we are using concurrent executions.
The race condition has been handled and it’s working as expected by utilizing the full power of modern day CPU's. Congrats⭐ on top of that it’s absolutely thread safe.

Now you can access the same instance variable wherever you want without any conflicts.

Summary

Awesome 🔥, you have successfully completed this tutorial. I would 💝 to hear your feedback and comments on the great things you're gonna build with this.
If you are struck somewhere feel free to comment. I am always available.

Please find the complete code at github

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free