Have you ever wondered how to translate some traditional design patterns to Golang, a language without classes or the OO concept of inheritance? Then you're in the right place. I'm going to go through a couple different ways to implement the singleton pattern.

What are the requirements of a Singleton?

To put it simply, in traditional OO a singleton is a class of which only a single instance may exist at any given time in a program.

  1. The instance should be initialized when it is first used, and then cached for later reuse.
  2. No matter how many times the creation mechanism is used, there can only be one instance.
  3. The instance should be accessible globally for use across component/module boundaries.

Initial attempt

The simplest way to illustrate this is with a bit of a naïve implementation. First, the code.

package first_attempt

var State int

func GetSingleton() int {
	if State == 0 {
		State = 42
	}
	return State
}

So now we test the different conditions.

package first_attempt

import (
	"testing"

	testify "github.com/stretchr/testify/require"
)

func TestGetSingleton(t *testing.T) {
	assert := testify.New(t)

	initial := GetSingleton()
	assert.Equal(42, initial, "State wasn't initialized as expected")

	initial += 5
	gotAgain := GetSingleton()
	assert.Equal(42, gotAgain, "State was changed!")

	// State is able to be changed to a new "instance", doesn't meet requirements.
	State = 43
	assert.Equal(43, GetSingleton())
}

As you can see, the function initializes the state, returns that state with repeated calls, and it's available globally... too available. Since the variable is exposed for direct tampering, anything could change the "instance". This could be resolved with a "setter" like other languages like to do, but this is still setting a new value instead of updating the existing value. To make matters worse, it's returned by value, so it's not even the same instance with subsequent function calls, it just happens to be the same value. This is a problem if two components are relying on the Singleton for shared state because they're actually getting two completely different instances.

So some changes are necessary:

  • Singleton should be returned as a pointer to allow for shared state scenarios.
  • Instance variable should not be visible globally.
  • The test case is able to mutate the State variable, but the underlying value should be encapsulated, not replaced with assignment.

Take 2

Okay, so the state should be within a struct, and changes should be brokered through encapsulation. Let's do it.

Note: The concept of "getters" and "setters" isn't nearly as popular/idiomatic in Go as it is with other languages, but it's used here to illustrate some simplistic encapsulation.
type Singleton struct {
	value int
}

func (s *Singleton) GetValue() int {
	return s.value
}

func (s *Singleton) SetValue(newValue int) {
	s.value = newValue
}

var state = Singleton{}

func GetSingleton() *Singleton {
	if state.value == 0 {
		state.value = 42
	}
	return &state
}

This should solve our problems, right? Well, there are other problems.

func TestGetSingleton(t *testing.T) {
	assert := testify.New(t)

	initial := GetSingleton()
	assert.Equal(42, initial.GetValue(), "State wasn't initialized as expected")

	gotAgain := GetSingleton()
	initial.SetValue(47)
	assert.Equal(47, gotAgain.GetValue(), "State was NOT changed!")

	// State value is able to be manipulated directly, may bypass validation checks in methods.
	gotAgain.value = 42
	assert.Equal(42, gotAgain.GetValue())

	// State variable is able to be changed to a new instance from within the same package, doesn't meet requirements.
	state = Singleton{
		value: 43,
	}
	assert.Equal(43, GetSingleton().GetValue())
}

Since the test is running in the same package, it illustrates that the value (Singleton) can still be changed to a different instance. The value can even be changed directly, bypassing potential validation in the setting method.

In practice, however, this is generally good enough to provide the expected result to code outside of the package, because only code inside the package can mess with the field/var that is not exposed.

Function Closure

For the extra paranoid, we'll need to get a little more creative to absolutely protect our value and require changes to always go through the accessors. This isn't bulletproof either, as it's susceptible to the same package scoping allowances as the previous example, but the notable difference is that the underlying value cannot be accessed by anything else directly. This is possible with function closures.

type Singleton interface {
	GetValue() int
	SetValue(newValue int)
}

type implProxy struct {
	getValue func() int
	setValue func(int)
}

func (p *implProxy) GetValue() int {
	if p.getValue != nil {
		return p.getValue()
	}
	return 0
}

func (p *implProxy) SetValue(newValue int) {
	if p.setValue != nil {
		p.setValue(newValue)
	}
}

var getSingleton = func() func() Singleton {
	var value int
	var impl Singleton
	return func() Singleton {
		if impl == nil {
			// Lazy init
			value = 42

			impl = &implProxy{
				getValue: func() int {
					return value
				},
				setValue: func(newValue int) {
					value = newValue
				},
			}
		}
		return impl
	}
}()

func GetSingleton() Singleton {
	return getSingleton()
}

Since value is contained within a local function scope, it can't be accessed outside of the function, but it's available inside the contained function because the variable is defined in that function's outer scope. Even if the user peeks behind the interface to the underlying implProxy struct, the value isn't there, and can't be retrieved without the getValue function.

Of course, the struct's fields can be changed by code in the same package, and so can the getSingleton function variable. Since function calls are not const expressions, getSingleton cannot be a const to prevent this. Despite these factors, this is likely a more resilient way to express the pattern with some assurance that package code won't be messing with the underlying value in an unexpected way.

Additional Tweaks

Often, structs require external data to initialize themselves. To do this with the closure scenario presented above, we could tweak it to provide an InitSingleton function that allows those parameters to be passed to the closure's outer scope. Here's one way to do it.

var getSingleton func() Singleton

func InitSingleton(initParam int) {
	// Prevent double initialization
	if getSingleton == nil {
		var value int
		var impl Singleton
		// Assign the function
		getSingleton = func() Singleton {
			if impl == nil {
				// Init immediately from value in outer context
				value = initParam

				impl = &implProxy{
					getValue: func() int {
						return value
					},
					setValue: func(newValue int) {
						value = newValue
					},
				}
			}
			return impl
		}
	}
}

func GetSingleton() Singleton {
	if getSingleton == nil {
		panic("Attempting to retrieve uninitialized singleton")
	}
	return getSingleton()
}

This pattern can make the singleton a lot easier to test, regardless of whether initialization parameters are needed. If multiple test cases in the same package need to test different aspects of the singleton, but they are all expecting a particular base state, a new instance may need to be created to establish that.

This generally should not be allowed because it breaks the rules of a singleton, but it could be accomplished by explicitly changing the getSingleton variable to nil, which allows the InitSingleton function to be called again with the expected base state. Otherwise, testing singletons can be rather challenging, depending on the complexity of the system and the singleton type.