Welcome to the #1 post of the “golang gotchas” series. Today we will talk about the Singleton Pattern. This series is organised as follows:
- code samples,
- gotchas areas, and
We know developers are very efficient in reading blogs and sort; hence wouldn’t it be nice if we can get all the code samples and gotchas in the first 10 minutes? Whilst the boring (but important) theories are left at the very end of the reading. Now we do have a choice, to know more through the theories or simply just get moving on your code :)
Code — how it works
The source code is available at: https://github.com/quoeamaster/golang_blogs/tree/master/2021-05-gg-singleton
And the code project’s folder structure as follows:
we will be creating a simple Logger object to log down all sorts of messages to an output. For simplicity, all messages would be forwarded to Standard-Out.
we defined a struct named “Logger” and add a function “Log” which simply print the provided “msg” to Standard-Out. We also declared a Mutex variable to “lock” and “unlock” blocks of code and guaranteed only one thread / go-routine can access those code blocks 1 at a time. A variable of type *Logger is also introduced here and this variable is the singleton instance.
By now, we need to add an accessor function to get the singleton reference — “GetLogger”. Clearly this function should be thread-safe and hence we would use the Mutex to lock and unlock the code. The code block is straightforward, if the instance is nil (not instantiated yet), create the instance. Finally the function returns the reference of the singleton object.
To verify the code, let’s write a unit test under the “test” package / folder:
in our test, we would create 10 threads / go-routines and each of them would access the logger singleton instance and log a message to standard-out. At the end, we would also check whether all the logger reference involved are sharing the same memory address (which SHOULD be).
the “waitGroup” object would help us to wait for the 10 threads / go-routines to finish their logging mission; do pay attention to these functions:
- “Add” — increase the group’s counter,
- “Done” — decrease the group’s counter and
- “Wait” — wait till the group’s counter drop back to 0.
during each routine, the memory address value of the Logger reference would be recorded in the “msgArray” variable. At the end, a check on whether all memory address values are identical would be run. Do note that if the values aren’t identical, the unit test will fail and exit at once.
Gotchas — anything to note or avoid
the above implementation is the easiest way to enforce a singleton pattern. The loophole, however, is we “assume” the developer would call the “GetLogger” function instead of making a call with the “new” operator.
For a small development team, enforcing the “GetLogger” function call is possible; however once the team is growing bigger… this seems unrealistic. Hence we need to re-factor the code by introducing an INTERFACE:
we added an ILog interface with the “Log” function declared. Next is create a private implementation named “loggerImpl”. As you see the loggerImpl exactly mimics the logging feature from our previous Logger struct.
As usual, we declare a private variable, but this time the variable type is ILog instead of loggerImpl. The corresponding access function is pretty much the same and we return the reference of the singleton after necessary instantiation.
PS. declaring the singleton type to the interface is the MAGIC here, we never return a solid implementation (i.e. the actual struct). Now we can have a private implementation instead of a public, hence nobody can use the “new” operator to create the logger~
To verify the behaviour, let’s again write another unit test:
the test function is nearly identical to our previous one, the only difference is we are replacing the “GetLogger” function with “GetLoggerImpl”, everything should work.
If we try to use the “new” operator to create an instance of “loggerImpl”; it is NEVER possible since this struct is of private scope. Hooray~ we just hid the mastermind — loggerImpl :)))
Theories — why, what and how
Q. Why do we need the singleton pattern? Doesn’t it make sense to create a new instance every time?
- For most cases, YES, we probably would create a new instance of an object / struct per use-case. But think about this scenario — for common and sharable features, declare the code in a static function might do the trick; what about if this function also keeps states? Take an example, a reference to a database connector, you can either introduce this reference as the static function’s parameter and make the function definition getting more clumsy and worst… you just broke the code’s integration (changing the function definition means several code blocks in the project would be affected at once). A better solution is to create an object that holds this database connector reference but still provide nearly static access, and this is why we have the singleton pattern.
- A simple word — if you are creating objects that could not be shared due to the states are non identical; then simply “new” the instance per request. If you are creating objects that bear a sharable state; then singleton pattern is worth the consideration.
Congratulations~ We have just gone through the short journey of Singleton in golang.
- learned how Mutex helps to guarantee thread-safe,
- how the singleton pattern could be coded,
- why the singleton instance / reference should be an Interface instead of the actual implementation (i.e. the targeted struct)
I hope you enjoyed. Till next time for more gotcha adventures :)