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
  • and provides a global access point to it.

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.
  2. Provide a global access point. Please note that this is not just a global variable through which you can reach a specific object. The global variable does not protect you from overwriting the created object.

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:

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:

We initialize a variable of type singleton equal to the empty value 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:

And the object return function:

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

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

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:

Run code:

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

At the output, we will see the following:

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:

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

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.
  2. Provides a global access point to it.
  3. Implementation of lazy initialization of an object.

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.
  2. Disguises bad code design.
  3. Multithreading issues for languages ​​like Golang. Considered in today’s lesson one of the ways how the problem is solved.
  4. For automated testing, requires the creation of mocks. Such objects are a concrete dummy implementation of an interface, intended solely for testing and interoperability between objects. In procedural languages, this is called dummy.

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!