Singleton Pattern in Golang

Vishal Jain
Towards Dev
Published in
6 min readJun 3, 2023

--

A Singleton pattern will provide us with a single instance of an object, and guarantee that there are no duplicates.
At the first call to use the instance, it is created and reused between all the parts in the application that need the use of particular behavior.

Common usage of singleton patterns around us:

1. Database connection
2. SSH connection to a server
3. If you need to limit the access to some variable or space, you use a Singleton as the door to this variable.
And many more…

General Guidelines we follow whenever using the Singelton pattern:

1. We need a single, shared value, of some particular type
2. We need to restrict object creation of some type to a single unit along the entire program.

To showcase the singleton pattern, let’s try an example

Unique Counter
We will write a counter that holds the number of times it has been called during program execution. It shouldn’t matter how many instances we have of the counter, all of them must count the same value and it must be consistent between the instances.

Requirements and acceptance criteria :

There are some requirements and acceptance criteria to write the described single counter. They are as follows:
1. When no counter has been created before, a new one is created with the value 0
2. If a counter has already been created, return this instance that holds the actual count
3. If we call the method AddOne, the count must be incremented by 1

Implementation:

We will implement a Singleton pattern. We’ll usually write a static method and instance to retrieve the Singleton instance in other languages like Java or C++.

In Go we don’t have the static, but we can achieve the same result by using the scope of the package.

First, we will create a struct containing the object we want. guarantee to be Singleton during the execution of the program:

package singleton

import (
"log"
"sync"
)

type Singleton interface {
AddOne() int
}

type singelton struct {
count int
}

var instance *singelton

// GetInstance Basic instantiation of singleton object
func GetInstance() Singleton {
if instance == nil {
log.Println("A new instance was created.")
instance = new(singelton)
} else {
log.Println("Single instance was already created")
}

return instance
}

func (s *singelton) AddOne() int {
s.count++
return s.count
}

We created a GetInstance method that checks if the instance has not been initialized already (instance == nil), and creates an instance in the space already allocated in the line instance = new(singleton). Remember, when we use the keyword new, we are creating a pointer to an instance of the type between the parentheses.

The AddOne method will take the count of the variable instance, raise it by 1, and return the current value of the counter.

Let’s execute the program and check if the Singleton contract is followed

package main

import "github.com/Vishalj32/go-design-patterns/creational/singleton"

func main() {
for i := 0; i < 5; i++ {
singleton.GetInstance()
}
}
vishaljain@Mac go-design-patterns % go run main.go 
A new instance was created.
Single instance was already created
Single instance was already created
Single instance was already created
Single instance was already created

In the above implementation, we have our first approach implementing Singleton in Golang and with goroutines to simultaneously run our Singleton.

The problem visible in this code is that several goroutine routines could evaluate the first check and all would create a singleton instance and replace one another. There is no guarantee of which instance will be returned in the code above and other additional operations on the instance may be inconsistent with developer expectations and stealth problems may occur at run time.

Implementation with Locks & Mutex

In this implementation, we will create a thread-safe Singleton instance access. We will be utilising sync.Mutex package to create a lock while creating a new instance.

package singleton

import (
"fmt"
"log"
"sync"
)

type Singleton interface {
AddOne() int
}

type singelton struct {
count int
}

var instance *singelton
var lock = &sync.Mutex{}

// GetInstanceMutex thread-safe singleton instantiation with single lock
func GetInstanceMutex() Singleton {
lock.Lock()
defer lock.Unlock()

if instance == nil {
log.Println("A new instance was created.")
instance = new(singelton)
} else {
log.Println("Single instance was already created")
}
return instance
}

func (s *singelton) AddOne() int {
s.count++
return s.count
}

This implementation also has a drawback, problem with this approach is excessive blocking even when it would not be necessary to do so in case the instance has already been created and should simply have returned the singleton instance. If our program is written to become highly concurrent, this can generate a bottleneck, since only one goroutine routine can get the singleton instance at a time, making it our slower solution.

Let’s try another approach.

Check-Lock-Check Implementation

One way to improve and ensure a minimum lock and still be safe for the goroutine is to use the pattern called “Check-Lock-Check” when acquiring locks.
The pattern works with the idea of checking first, to minimize any aggressive blocking, since an IF statement is less expensive than locking. Next time, we would have to wait and get the exclusive lock so that only one execution is inside that block at a single time. With the first scan and exclusive lock, there may be another goroutine that has the lock, so we would need to double-check inside the lock to avoid replacing the instance with another.

package singleton

import (
"fmt"
"sync"
"sync/atomic"
)

type Singleton interface {
AddOne() int
}

type singelton struct {
count int
}

var instance *singelton
var lock = &sync.Mutex{}

// GetInstanceMutexDoubleCheck thread-safe singleton instantiation with double lock
func GetInstanceMutexDoubleCheck() Singleton {
if instance == nil {
lock.Lock()
defer lock.Unlock()

if instance == nil {
fmt.Println("A new instance was created.")
instance = new(singelton)
} else {
fmt.Println("Single instance was already created")
}
} else {
fmt.Println("Single instance was already created")
}

return instance
}

This approach is faster and much safer, but it is still not perfect. Because there is no atomic check on the instance storage.

Using the sync/atomic package, we can load and set atomically a flag that indicates whether or not we initialize our instance.

Check the code below:

package singleton

import (
"fmt"
"sync"
"sync/atomic"
)

type Singleton interface {
AddOne() int
}

type singelton struct {
count int
}

var instance *singelton
var lock = &sync.Mutex{}
var atomicInt uint64

// GetInstanceAtomic creates an instance using atomic variable and lock
func GetInstanceAtomic() Singleton {
if atomic.LoadUint64(&atomicInt) == 1 {
fmt.Println("Single instance was already created")
return instance
}
lock.Lock()
defer lock.Unlock()

if atomicInt == 0 {
instance = new(singelton)
atomic.StoreUint64(&atomicInt, 1)
}
fmt.Println("A new instance was created.")
return instance
}

All these approaches have their own benefits and drawbacks. They are also a little complicated to implement and work around ease-fully.

Let’s look into a Golang-specific approach that is clean and more powerful.

sync.Once Implementation

We have the type “Once” in the sync library, remembering that this native library in Golang is very powerful and we have to exploit it as much as we can, the sync. Once an object will perform an action exactly once and not more, it was what was missing for our code to get even more powerful and clean.

package singleton

import (
"fmt"
"sync"
"sync/atomic"
)

type Singleton interface {
AddOne() int
}

type singelton struct {
count int
}

var instance *singelton
var once sync.Once

// GetInstanceSyncOnce singleton instantiation using sync.Once package
func GetInstanceSyncOnce() Singleton {
if instance == nil {
once.Do(
func() {
fmt.Println("A new instance was created.")
instance = new(singelton)
})
} else {
fmt.Println("Single instance was already created")
}
return instance
}
vishaljain@Mac go-design-patterns % go run main.go 
A new instance was created.

Conclusion

We have seen multiple implementations of the singleton pattern. Just keep in mind that the Singleton pattern will give you the power to have a unique instance of some struct in your application and that no package can create any clone of this struct.

The ideal is undoubtedly the use of sync.Once that guarantees us the uniqueness and that is “Thread Safe” guaranteeing that a “Race Conditiondoes not occur, it only allows the function to be executed only once Golang flexibility and automated all the complexity we would have in other languages if we were to work with competition and simultaneity

With Singleton, you are also hiding the complexity of creating the object, in case it requires some computation, and the pitfall of creating it every time you need an instance of it if all of them are similar. All this code writing, checking if the variable already exists, and storage, are encapsulated in the singleton and you won’t need to repeat it everywhere if you use a global variable.

--

--

Learning a new thing at a time, problem solver and a knowledge seeker , SDE-II at Bookmyshow, ex-Connectwise, Zycus