The subtleties of the Singleton implementation in Golang

Hi friends. My name is Alex Versus and today we will take a look at the Singleton pattern, an implementation in the Golang language.

What’s the point?

Singleton — refers to creational patterns. Ensures that:

  • a class / type has only one instance

What problem does it solve?

Let’s talk about the problem that the pattern solves. A loner solves two problems at once, violating the Single Responsibility Principle (SRP):

  1. Ensures that there is a single instance of an object. This is useful for accessing a shared resource, such as a database, or when implementing a single mechanism for changing a property, such as the sound level in an equalizer. Let’s imagine that we have some kind of object and after a while you create another one, but you would like to receive not a new, but already created object. This behavior cannot be created using standard tools such as the constructor in object-oriented languages.

Developers often call Singleton-objects that perform only one task, as outlined above. This is a misunderstanding of the pattern.

What’s the solution in Golang?

How to solve the voiced tasks in GOlang? Anyone familiar with the implementation of the pattern in OOP, know that you need to hide the constructor and declare a public static method that controls the life cycle of a single object. The static method will provide access to the object from anywhere in your program. You can see the implementation here. No matter how much you call this method, it will always return the same object. The template class diagram is as follows:

Class diagram Singleton

GOlang has no classes or constructors. But there are types and structures. How do we implement the getInstance() method? Let’s create a specific type of singleton:

// declaration defined type 
type singleton struct {

}

We initialize a variable of type singleton equal to the empty value nil:

// declare variable
var instance *singleton = nil

To set a value in instance, we need to use the sync.Once method of the standard library. It takes a function as an argument, which will be executed once per call. And also we must define the Sigleton type with you and define an interface in it with methods for working with properties of our type:

// defined type with interface
type Singleton interface {
// here will be methods
}

And the object return function:

/**
Get only one object
*/
func GetInstance() Singleton {
once.Do(func() {
instance = new(singleton)
})
return instance
}

To test the implementation, we will add a field to the singleton type, which we will change using setters and getters:

// declaration defined type
type singleton struct {
title string
}

And we define methods in the interface of the Singleton type that allow you to change the value of this property:

// defined type with interface
type Singleton interface {
SetTitle(t string)
GetTitle() string
}
/**
Setter for singleton variable
*/
func (s *singleton) SetTitle(t string) {
s.title = t
}
/**
Getter singleton variable
*/
func (s *singleton) GetTitle() string {
return s.title
}

Let’s implement the code for working with the property of these objects. Now we need to check how our code works. Let’s write a small test and see that everything works correctly with this implementation:

package Singletonimport “testing”func TestGetInstance(t *testing.T) {
var s Singleton
s = GetInstance()
if s == nil {
t.Fatalf(“First sigletone is nil”)
}
s.SetTitle(“First value”)
checkTitle := s.GetTitle()
if checkTitle != “First value” {
t.Errorf(“First value is not setted”)
}
var s2 Singleton
s2 = GetInstance()
if s2 != s {
t.Error(“New instance different”)
}
s2.SetTitle(“New title”)
newTitle := s.GetTitle()
if newTitle != “New title” {
t.Errorf(“Title different after change”)
}
}

Run code:

go test -v -run TestGetInstance
=== RUN TestGetInstance
— — PASS: TestGetInstance (0.00s)
PASS
ok main/Singleton 0.310s

Excellent! It seems that everything is ok, but in fact, no I want to show you another test that will show what problem exists:

package Singletonimport (
“fmt”
“strconv”
“sync”
“testing”
)
func TestSecondGetInstance(t *testing.T) {
s1 := GetInstance()
s2 := GetInstance()
var w sync.WaitGroupfor i := 0; i < 3000; i++ {
j := i
w.Add(1)
go func() {
t := “title_” + strconv.Itoa(j)
s1.SetTitle(t)
w.Done()
}()
w.Add(1)
go func() {
t2 := “title_2_” + strconv.Itoa(j)
s2.SetTitle(t2)
w.Done()
}()
}
fmt.Println(s1.GetTitle())
fmt.Println(s2.GetTitle())
}

At the output, we will see the following:

go test -v -run TestSecondGetInstance
=== RUN TestSecondGetInstance
title_2998
title_2_2999

A loop of 3000 iterations is started in which two goroutines are created, in which the property setting method is called. The method changes property values ​​every iteration using a setter. After executing the code block, we expect the property values ​​of the single objects to be the same, but this is not the case. Why is this happening?

By adding the -race option to the test run command, we will see that a so-called data race is taking place. This happens in Golang when two goroutines are simultaneously working on the same variable and one of the threads writes to this variable. This is a multithreading problem.

There are several ways to solve the problem. We will not dwell on this topic in detail, today we are talking about the implementation of the Singleton pattern. I’ll just show you one of the solutions. The solution is called mutual exclusion and can be achieved using a mutex. Conventionally, a classic mutex can be represented as a variable that can be in two states: locked and unlocked. When entering its critical section, a thread calls the function of transferring the mutex to the blocked state, and the thread is blocked until the mutex is released, if another thread already owns it. In Go, it can be implemented through the standard library which has the sync.Mutex and sync.RWMutex sync primitives. The implementation looks like this:

// declaration defined type
type singleton struct {
title string
sync.RWMutex
}
/**
Setter for singleton variable
*/
func (s *singleton) SetTitle(t string) {
s.Lock()
defer s.Unlock()
s.title = t
}
/**
Getter singleton variable
*/
func (s *singleton) GetTitle() string {
s.RLock()
defer s.RUnlock()
return s.title
}

Let’s run the second test again and see that now both variables are equal:

go test -v -run TestSecondGetInstance
=== RUN TestSecondGetInstance
— — PASS: TestSecondGetInstance (0.00s)
PASS

This is the implementation of the Singleton design pattern in Golang.

What are the advantages of a pattern?

  1. We guarantee the presence of a single object in the program.

What are the disadvantages?

  1. Violates the single responsibility principle for OOP languages. Each object / class / type should have only one responsibility and the observance of this responsibility should be encapsulated, methods should solve the problems of this responsibility.

Singleton is a very deceiving pattern, you often want to use it, but you need to do it very carefully. For example, if we want to create a single volume control when developing software for an equalizer, we assume that volume will be a global property of the program. However, if we want to introduce a separate control for the volume of some notifications, a second Singleton will appear, and then dependencies between instances may appear. Violations of the Single Responsibility Principle (SRP), according to which each entity should have only one responsibility, and all its methods should be aimed at fulfilling it. If we implement Singleton, then our type, in addition to its main work, can begin to control the number of instances created. Singleton is a global object, with all the ensuing problems.

If there is an error in the program and it depends on a global object, the state of which could be changed by anyone, then it is much more difficult to find it. Or another example — we can start writing unit tests that always depend on each other if they use the same global object.

And that’s all for today. I hope I helped you understand the intricacies of implementing practice in the Golang language.

I was glad to share the material. Alex Versus. Good luck to all!