Monday, January 13, 2025

Singleton

Profile Pic of Akash AmanAkash Aman

Updated: January 2025

📝 Brief

Singleton is a creational design pattern 🏁 that ensures that only one instance of a class is created and provides a global 🌎 point of access to that instance.

Here’s how it works: imagine that you created an object, but after a while decided to create a new one. Instead of receiving a fresh object, you’ll get the one you already created.

❓ Problem

The Singleton pattern solves two problems at the same time:

  • 🚀 Ensure that class has single instance.

    But Why 🤔 might someone want to limit how many instance of a class can be created ? Well, the most common reason for this is to control access to some shared resource—for example, a database or a file 📂.

  • 🌎 Provide a global access point to that instance.

    Remember, providing a global access point to a Singleton can be very unsafe in concurrent or multi-threaded environments. If its internal state is directly modified or its instance is exposed without proper safeguards, it can lead to race conditions, inconsistent data, or even application crashes.

    We will explore 🚀 this in more detail as we move forward in this article 😃.

💡 Solution

Let’s talk about how we can implement the Singleton pattern:

  1. 🔒 Restricting Direct Instantiation:
    First, we need to make sure no one can create instances of our class directly. This means hiding the constructor. Why? Because if anyone could just new up an instance, we’d lose control over the “single instance” rule!

  2. 🚪 Providing Controlled Access:
    Next, we give users a safe doorway to access the Singleton. A static or global method (often called something like GetInstance) will handle this. If the instance doesn’t exist yet, this method creates it. Otherwise, it returns the already created one.

  3. 🧵🔒 Making It Thread-Safe:
    In multi-threaded environments, things get tricky. Imagine two threads checking if the instance exists at the same time—boom, we end up with two instances! To prevent this, we’ll add a synchronization mechanism (like sync.Once in Go) to guarantee that the instance is created only once, no matter how many threads try to access it.

🧬 Blueprint

SingletonClassvariableMethodsDatabase Instance<Database>GetInstance()ApplicationAuthAPI

🛠️ Implementation & Analysis

There two ways to make singleton instance, aggressive & Lazy Initialization of instance.

Note:
  • If you choose aggressive initialization (i.e., initializing directly within the constructor instead of creating a separate method and calling it only when necessary) for every class in your application, it may slow down the initial app startup time.

  • However, this is only possible in languages that support constructors (e.g., C#), not in Golang, which relies on factory functions instead.

  • Private Constructor: Make the class's constructor private to prevent direct instance creation by other objects.

  • Create Storage: Add a private static field to hold the singleton instance.

  • Get Instance Method: Declare a public static method to provide access to the singleton instance. Use lazy initialization in this method, creating the instance only if it doesn’t already exist, ensuring resources are allocated only when needed.

    For certain applications, eager initialization in the constructor may be preferred, especially for critical or resource-heavy instances.

  • Update Client Code: Replace any direct instance creation in the codebase with calls to the static GetInstance method.

Lets understand with an example.

In this example, we’ll implement a simple Counter using the Singleton pattern. Our goal is to ensure that only one instance of the Counter exists, and we’ll demonstrate how lazy initialization works—only creating the instance when it’s first needed. You’ll also see how we can manipulate the shared state of the singleton instance and ensure that all parts of the application access and modify the same counter value.

Step 1: Defining the Singleton Instance

In Go, we start by defining a global variable to store the singleton instance. In our example, the singleton will be a Counter struct.

go
var ( instance *Counter // Holds the singleton instance )

Here, instance is a *Counter pointer. Initially, it’s set to nil, and we’ll populate it with an actual instance of Counter when it’s first needed.

Step 2: Lazy Initialization of the Instance

Now, let's define a function to retrieve the singleton instance. The key idea behind the Singleton pattern is lazy initialization, where we only create the instance if it hasn’t been created yet. This is done by checking if instance is nil.

go
func GetInstance(initialCounter int) *Counter { if instance == nil { fmt.Println("Initializing Counter with counter:", initialCounter) instance = &Counter{counter: initialCounter} } return instance }
  • If instance is nil, we initialize it with a Counter object that starts with a value provided by initialCounter.
  • Once the instance is created, future calls to GetInstance will return the same instance, ensuring that no new instances are created.

Step 3: Adding Methods to the Singleton

Let’s add some methods to the Counter struct to modify and access the internal state.

go
func (s *Counter) Increment() { s.counter++ fmt.Println("Counter:", s.counter) } func (s *Counter) GetCounter() int { return s.counter }
  • Increment: This method increases the counter by 1 and prints the updated value.
  • GetCounter: This simply returns the current value of counter.

With these methods, we can manipulate and access the singleton instance’s state.

Step 4: Using the Singleton in the Main Function

Now that we’ve set up our Singleton, let’s see it in action in the main function.

go
func main() { // Get the singleton instance with an initial counter value of 10 counter1 := GetInstance(10) counter1.Increment() // Increments counter to 11 // Get the same singleton instance (initial value doesn't matter) counter2 := GetInstance(100) counter2.Increment() // Increments counter to 12 // Verify that both variables point to the same instance fmt.Printf("Are counter1 and counter2 the same? %v\n", counter1 == counter2) // Print the final counter value fmt.Println("Final Counter Value:", counter1.GetCounter()) }

What Happens Here?

  • First Call: When GetInstance(10) is called, it initializes the Counter with a value of 10 and returns the instance. We then call Increment, which increases the counter to 11.
  • Second Call: When GetInstance(100) is called, it doesn’t create a new instance. Instead, it returns the existing instance (from the first call). We then call Increment again, increasing the counter to 12.
  • Singleton Check: We print whether counter1 and counter2 are the same instance. Since they reference the same object, the output will be true.
  • Final Counter Value: The final counter value will be 12, confirming that both counter1 and counter2 share the same instance and changes made through one reference affect all.

🧑‍💻 Putting It All Together

Here’s the complete Singleton implementation:

go
package main import ( "fmt" ) type Counter struct { counter int } var ( instance *Counter ) func GetInstance(initialCounter int) *Counter { if instance == nil { fmt.Println("Initializing Counter with counter:", initialCounter) instance = &Counter{counter: initialCounter} } return instance } func (s *Counter) Increment() { s.counter++ fmt.Println("Counter:", s.counter) } func (s *Counter) GetCounter() int { return s.counter } func main() { counter1 := GetInstance(10) counter1.Increment() counter2 := GetInstance(100) counter2.Increment() fmt.Printf("Are counter1 and counter2 the same? %v\n", counter1 == counter2) fmt.Println("Final Counter Value:", counter1.GetCounter()) }

🧵 Ensuring Thread Safety

Have you noticed something? 🤔 The code above isn't safe for a multi-threaded environment. Let’s explore why this happens and how we can make it thread-safe.

Our Singleton implementation worked fine in a single-threaded environment, but what happens when multiple threads enter the picture? Let’s break it down:

  1. Instance Creation:
    Imagine two threads calling GetInstance at the exact same time. Both threads check if instance is nil. Finding it empty, both proceed to create a new Counter instance. Now, we have two instances. Oops! There goes the Singleton guarantee.

  2. State Modification:
    Our Increment and GetCounter methods directly manipulate and access the counter. But what happens if two threads try to increment the counter at the same time? You guessed it—race conditions, inconsistent data, and a whole lot of chaos.

Clearly, we need a fix.

🚧 Step 1: Fixing Instance Creation

To ensure only one Counter instance is created—no matter how many threads call GetInstance—we can use sync.Once.

Think of sync.Once as a bodyguard for our Singleton. It makes sure that no matter how many threads try to create the instance, only one thread succeeds.

Here’s how it looks:

go
var ( instance *Counter once sync.Once ) func GetInstance(initialCounter int) *Counter { once.Do(func() { fmt.Println("Initializing Counter with counter:", initialCounter) instance = &Counter{counter: initialCounter} }) return instance }

🔑 What changed?

  • The once.Do block is executed exactly once, ensuring that instance is created only once, no matter how many threads call GetInstance.

🚧 Step 2: Fixing State Modification

Now let’s address the second issue: race conditions.

We’ll add a sync.Mutex to our Counter struct. This ensures that only one thread can modify or access the counter at a time.

Here’s the updated code:

go
type Counter struct { counter int mutex sync.Mutex // Protects the counter } func (c *Counter) Increment() { c.mutex.Lock() defer c.mutex.Unlock() c.counter++ fmt.Println("Counter incremented to:", c.counter) } func (c *Counter) GetCounter() int { c.mutex.Lock() defer c.mutex.Unlock() return c.counter }

🔑 What changed?

  • The mutex.Lock and mutex.Unlock calls ensure that each thread gets exclusive access to the counter when reading or updating it.

🧑‍💻 Putting It All Together

Here’s the complete thread-safe Singleton implementation:

go
package main import ( "fmt" "sync" ) type Counter struct { counter int mutex sync.Mutex } var ( instance *Counter once sync.Once ) func GetInstance(initialCounter int) *Counter { once.Do(func() { fmt.Println("Initializing Counter with counter:", initialCounter) instance = &Counter{counter: initialCounter} }) return instance } func (c *Counter) Increment() { c.mutex.Lock() defer c.mutex.Unlock() c.counter++ fmt.Println("Counter incremented to:", c.counter) } func (c *Counter) GetCounter() int { c.mutex.Lock() defer c.mutex.Unlock() return c.counter } func main() { var wg sync.WaitGroup // Simulate multiple threads accessing the singleton for i := 1; i <= 5; i++ { wg.Add(1) go func(id int) { defer wg.Done() counter := GetInstance(id * 10) // Initial value is ignored after the first call counter.Increment() }(i) } wg.Wait() fmt.Println("Final Counter Value:", GetInstance(0).GetCounter()) }

🌍 Practical Example

In this section, let’s build a real-world example to better understand the Singleton pattern. Imagine we’re creating a logging system for an application. The log file should be accessible globally, and there should only ever be one instance of the logger managing the log file.

Why is this important?

  • Consistency: All parts of the application write to the same log file.
  • Efficiency: We avoid the overhead of creating multiple loggers or opening/closing files repeatedly.
  • Centralized Control: A single instance ensures global access and synchronized logging in a multi-threaded environment.

We’ll implement this logger step-by-step while ensuring:

  1. Thread safety using sync.Once.
  2. Controlled access to shared resources using sync.Mutex.

🛠 Step 1: Define the Singleton Instance

First, we need a structure to represent our logger and a global variable to hold the singleton instance. We also introduce sync.Once to ensure that the instance is created only once, no matter how many threads call for it.

go
package main import ( "fmt" "os" "sync" ) type Logger struct { file *os.File // Represents the log file mu sync.Mutex // Ensures thread-safe logging } var ( loggerInstance *Logger // Singleton instance once sync.Once // Ensures instance is created only once )

🛠 Step 2: Create the Singleton

Now, let’s write a function to retrieve the singleton instance. If the instance doesn’t exist, we initialize it and open the log file for writing.

go
func GetLogger(fileName string) *Logger { once.Do(func() { // Ensures the instance is initialized only once file, err := os.OpenFile(fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { panic("Could not open log file") } loggerInstance = &Logger{file: file} }) return loggerInstance }

🛠 Step 3: Add Logging Functionality

Next, we add a Log method to write messages to the log file. To ensure thread safety, we use a sync.Mutex to lock the operation.

go
func (l *Logger) Log(message string) { l.mu.Lock() // Ensure only one thread writes at a time defer l.mu.Unlock() // Unlock after logging fmt.Fprintln(l.file, message) }

🛠 Step 4: Use the Logger in main

Finally, let’s demonstrate the singleton in action.

go
func main() { // Get the singleton instance and log messages logger := GetLogger("app.log") logger.Log("Application started") // Get the same logger instance and log another message logger2 := GetLogger("ignored.log") logger2.Log("Another log message") // Verify both instances are the same fmt.Printf("Are logger and logger2 the same? %v\n", logger == logger2) }

What Does This Example Show?

  1. Singleton Behavior:

    • The Logger instance is created only once.
    • Both logger and logger2 point to the same instance.
  2. Thread Safety:

    • The sync.Once ensures the instance is created only once, even in multi-threaded environments.
    • The sync.Mutex ensures no two threads write to the log file simultaneously.
  3. Practicality:

    • This pattern ensures that all parts of the application write to the same log file, maintaining consistency and avoiding resource conflicts.

⚖️ Pros & Cons

ProsCons
✅ You can be 100% sure there’s only one instance of the class in your app, which keeps things simple.❌ It breaks the Single Responsibility Principle—because it’s handling more than just object creation.
✅ Having a global access point to that instance makes it super easy to interact with whenever needed.❌ It might hide some underlying design issues, especially when different parts of your app get too familiar with each other.
✅ The Singleton object gets created only when it’s actually needed, so you save on resources.❌ If you’re working in a multithreaded environment, you'll need to be extra careful to avoid multiple threads creating their own instances.
❌ Testing is a pain because of the private constructor and static methods, so you might have to get creative to mock it for tests.

🕒 When to Use

  • Apply the Singleton pattern when you need a single, shared instance of a class throughout your program, like a central database object used by various components.

  • Use the Singleton pattern when you need stricter control over global variables.

    You might want to use the Singleton pattern when you need more control over global variables.

    The cool thing about Singleton is that it guarantees there’s only one instance of a class in your entire app. Unlike regular global variables, nothing—except the Singleton itself—can create or replace that instance.

  • Facade: Sometimes, a Facade can actually be turned into a Singleton. Why? Because a single facade object is usually enough to handle all the interactions.

    So, if you're already using a Facade and only need one instance of it, you might as well make it a Singleton.

  • Flyweight: The Flyweight pattern might remind you of Singleton because it’s all about sharing objects. But here's the twist: while a Singleton only has one instance, Flyweight can have multiple instances, each with different intrinsic states.

    So, even though they share some similarities, they serve different purposes. Also, a Singleton object can be mutable, but Flyweight objects are typically designed to be immutable.

🎯 Try Yourself

How would you create a thread-safe Singleton instance of a counter in a multi-threaded environment, allowing the counter to be incremented from different threads without causing any race conditions?