Design Pattern Series - Singleton Pattern (Golang)

Go Design Pattern Series Jun 3, 2025

Previous post in the series: Abstract Factory Pattern

Singleton Pattern in Go

The Singleton Pattern is a Creational Pattern that ensures a struct has only one instance throughout the application's lifecycle while providing global access to that instance. In this article, we’ll explore why to use the Singleton Pattern, how to implement it in Go, its drawbacks, and possible solutions.

When to Use the Singleton Pattern in Go?

The Singleton Pattern is typically used when you want to:

  • Control Resources: Ensure only one instance of a resource exists.
  • Global Access: Provide a single access point to a shared resource.
  • Optimize Performance: Avoid creating multiple resource-heavy instances.

Examples:

  • Managing database connections.
  • Handling configuration managers.
  • Logging services.
  • Caches.

Implementing Singleton in Go

Below are three ways to implement the Singleton Pattern in Go: basic, thread-safe with mutex, and thread-safe with sync.Once.

1. Basic Singleton

// singleton/singleton.go
package singleton

import "fmt"

type Singleton struct {
    data string
}

var instance *Singleton

func GetInstance() *Singleton {
    if instance == nil {
        instance = &Singleton{data: "Singleton Instance"}
        fmt.Println("Creating new Singleton instance")
    }
    return instance
}

Explanation:

  • instance is a package-level variable that holds the single instance of the Singleton struct.
  • GetInstance() checks if instance is nil. If it is, it creates a new instance; otherwise, it returns the existing one.
  • The data field demonstrates data stored in the Singleton.

Usage:

package main

import (
    "fmt"
    "yourmodule/singleton"
)

func main() {
    s1 := singleton.GetInstance()
    s2 := singleton.GetInstance()

    fmt.Println(s1 == s2) // true
    fmt.Println(s1.data)  // Singleton Instance
}

This is the simplest Singleton implementation, but it’s not thread-safe and may lead to race conditions in concurrent environments.

2. Thread-Safe Singleton with Mutex

// singleton/singleton.go
package singleton

import (
    "fmt"
    "sync"
)

type Singleton struct {
    data string
}

var instance *Singleton
var mutex = &sync.Mutex{}

func GetInstance() *Singleton {
    mutex.Lock()
    defer mutex.Unlock()

    if instance == nil {
        instance = &Singleton{data: "Thread-safe Singleton Instance"}
        fmt.Println("Creating new Singleton instance")
    }
    return instance
}

Explanation:

  • sync.Mutex locks the creation process to ensure only one goroutine can access the initialization code.
  • mutex.Lock() and defer mutex.Unlock() ensure the lock is released.
  • Prevents race conditions but incurs performance overhead due to repeated locking.

Usage:

package main

import (
    "fmt"
    "sync"
    "yourmodule/singleton"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            s := singleton.GetInstance()
            fmt.Println(s.data)
        }()
    }
    wg.Wait()
}

Output: Only one instance is created.

3. Thread-Safe Singleton with sync.Once

// singleton/singleton.go
package singleton

import (
    "fmt"
    "sync"
)

// Singleton struct represents the single instance
type Singleton struct {
    data string
}

// Package-level variables and sync.Once
var instance *Singleton
var once sync.Once

// GetInstance returns the single instance, using sync.Once
func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{data: "Thread-safe Singleton with sync.Once"}
        fmt.Println("Creating new Singleton instance")
    })
    return instance
}

Explanation:

  • sync.Once ensures the initialization function is called only once, regardless of how many goroutines call GetInstance().
  • More efficient than mutex as it checks the state only once.
  • This is the recommended approach in Go.

Usage:

package main

import (
    "fmt"
    "sync"
    "yourmodule/singleton"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            s := singleton.GetInstance()
            fmt.Println(s.data)
        }()
    }
    wg.Wait()
}

Output: Only one instance is created.

Drawbacks of the Singleton Pattern

While the Singleton Pattern offers benefits, it also has several drawbacks:

  • Violates Single Responsibility Principle (SRP): The Singleton handles both instance creation and business logic, violating the SRP from SOLID principles.
  • Creates Tight Coupling and Dependencies: As a global access point, it makes other components dependent on it, complicating maintenance, extensibility, and introducing errors during changes.
  • Testing Challenges: Global state can cause test cases to interfere with each other, and it’s hard to mock or replace the instance during unit testing.
  • Scalability Issues: Switching from a single instance to multiple instances (e.g., for load balancing) requires significant refactoring.
  • Concurrency Issues: If not implemented carefully, multiple instances may be created in concurrent environments, leading to logic errors or data loss.
  • Performance Bottlenecks: In high-concurrency scenarios, the Singleton can become a bottleneck due to multiple threads accessing it simultaneously.

Solutions to Mitigate Drawbacks

To address these issues, consider the following approaches:

  • Use Dependency Injection: Instead of accessing the Singleton globally, pass the instance to classes that need it.
  • Define an Interface for the Singleton: Makes it easier to replace or extend the Singleton’s behavior, especially for testing.
  • Design Thread-Safe Singletons: Use techniques like sync.Once (in Go), double-checked locking, or other synchronization mechanisms to ensure only one instance is created in concurrent environments.
  • Avoid Storing Mutable State in the Singleton: Prevent storing changing state to reduce the impact of global state on the system.
  • Provide Reset/Override Mechanisms for Testing: Allow overriding or resetting the Singleton instance in test environments to ensure independent test cases.
  • Use Singleton Only When Necessary: Carefully evaluate whether the Singleton is needed, and prioritize other patterns when possible.
  • Separate Instance Management from Business Logic: Ensure the Singleton only manages its lifecycle, not additional functionality.

Best Practices for Using Singleton

  • Use Singleton Only for Truly Unique Resources: Examples include database connections or global configurations.
  • Ensure Thread-Safety: Always implement thread-safe Singletons in concurrent environments.
  • Prefer Dependency Injection: Reduces dependencies and improves testability.
  • Define Singleton via Interface: Facilitates mocking and extensibility.
  • Avoid Mutable State in Singleton: Reduces risks of errors and unpredictability.
  • Provide Reset/Override for Testing: Ensures test independence if Singleton is unavoidable.
  • Clearly Manage Singleton Lifecycle: Prevent memory leaks.
  • Avoid Overusing Singleton: Use it sparingly to reduce coupling and improve scalability.
  • Document Clearly: Specify the purpose and scope of the Singleton in the project for proper use by team members.

Conclusion

The Singleton Pattern is a useful tool in Go for ensuring a single instance and providing global access. However, it must be implemented carefully, especially in concurrent environments. Using sync.Once is the recommended approach in Go.

Hopefully, this article provides a comprehensive understanding of the Singleton Pattern in Go. Apply the examples above and carefully consider whether the Singleton is suitable for your project.

Tags