Design Pattern Series - Singleton Pattern (Golang)
Previous post in series: Abstract Factory Pattern
Singleton Pattern is one of the Creational Patterns that makes sure one struct will have only one instance during the application life cycle, and provides global access to that instance.
In this article, we will learn why to use the Singleton Pattern, how to implement it in Go, its drawbacks, and solutions.
Use Cases of Singleton
The Singleton Pattern is often used when you want to:
- Resource Control: Ensures there is only one instance of a resource.
- Global Access: Provides a single point of access to shared resources.
- Performance Optimization: Avoids creating multiple, heavy instances.
Example:
- Manage database connections.
- Manage configuration managers.
- Logging services.
- Caches.
Implement Singleton in Go
Here are three ways to implement Singleton 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
}Explains:
instanceis a package-level variable that holds the unique instance..GetInstance()check ifinstance == nilthen create a new one, otherwise return the existing instance.- Attribute
datajust to illustrate the data in Singleton.
Uses:
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
}CONS: Not thread-safe, may cause race conditions in multi-threaded 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
}
Explain:
sync.Mutexused to lock when creating an instance.mutex.Lock()anddefer mutex.Unlock()Ensure only one goroutine is run that creates the instance.- Avoids race conditions but has overhead due to a lock on each call.
Uses:
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()
}Result: Only one intance was created
3. Thread-Safe Singleton with sync.Once
// singleton/singleton.go
package singleton
import (
"fmt"
"sync"
)
type Singleton struct {
data string
}
var instance *Singleton
var once 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
}Explain:
sync.Onceensures the constructor is called only once, no matter how many goroutines call itGetInstance().- More efficient
mutexbecause it checks the status once. - Here is the recommended implementation in Go.
Uses:
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()
}
Result: Just one instance was created
Disadvantages of Singleton Pattern
- Violating Single Responsibility Principle (SRP): Singleton manages lifecycle and contains business logic.
- High coupling and dependency: Difficult to maintain and expand.
- Difficult to test: Global state makes test cases affect each other, difficult to mock or replace in unit test.
- Limiting expansion: If you want to switch from single instance to multiple instances (e.g. load balancing) → have to refactor a lot.
- Concurrency issues: If not careful, you can create multiple instances in a multi-threaded environment. issues: Nếu không cẩn thận, có thể tạo nhiều instance trong môi trường đa luồng.
- Performance bottleneck: Singleton can become a bottleneck if accessed by multiple threads at the same time.mance bottleneck: Singleton có thể thành điểm nghẽn nếu bị truy cập bởi nhiều thread cùng lúc.
Solution:
- Use Dependency Injection: Instead of calling the global Singleton, pass the instance to the class that needs to be used.
- Define an Interface for the Singleton: Make it easy to replace or mock when testing.
- Design thread-safe: Use sync.Once in Go.
- Avoid storing mutable state: Limit global state that is easy to change.
- Provide reset/override when testing: Ensure test case independence.
- Use only when necessary: Evaluate carefully before applying.
- Separate lifecycle management from business logic: Let the Singleton only manage the instance.
Best Practices when using Singleton
- Only use for truly unique resources (DB connection, global config).
- Ensure thread-safety in concurrent environments.
- Prioritize Dependency Injection to reduce coupling.
- Define Singleton via interface for easy extension and testing.
- Avoid mutable state in Singleton.
- Document clearly the purpose and scope of use.
Conclude
The Singleton Pattern is a useful tool in Go to ensure there is only one instance and provide global access.
However, it needs to be implemented carefully, especially in concurrent environments.
The recommended implementation in Go: use sync.Once.
Thanks for reading!