I. Singleton - Creational Pattern
Singleton is probably the first design pattern that comes to mind when developers talk about design patterns. It is hard to find an interview about design patterns that does not mention Singleton at least once.
As the name suggests, Singleton means there is only one instance of a given type in the entire program. On the first call, that instance is created and then reused for every subsequent use.

II. When Should We Use Singleton?
Here are a few common cases where Singleton shows up in real projects:
- When we want to reuse a single database connection across multiple queries.
- When we open a Secure Shell (SSH) connection to a server and do not want to reopen it for every task.
- When we need to limit concurrent access to a variable or memory region. Singleton can act as a controlled entry point (often combined with a Connection Pool). In Golang, we can also use
channelsto handle this.
There are many more use cases — I am only listing a few here.
III. Real-world Example
Suppose we have a Print function that runs when the user clicks the Print order button, and a Counter object that tracks how many times the user clicked during the app lifetime. We must make sure the Counter is always the same instance. That is where Singleton helps (there are other ways to do this without Singleton, but they tend to get messy).
The user does not know that every Print action goes through the same Counter behind different entry points — like opening different doors that all lead to the same room. Source: Guru
For this to work correctly, Counter must follow these rules:
- If no
Counterhas been created yet, initialize it with value0.
- If a
Counteralready exists, return that same instance.
- Every time
Printis triggered, increment the counter by1.
IV. Implementation
I will build a small program to implement the Singleton Counter from the example above. The project structure looks like this:
main.go— entry point
singleton/package — Counter singleton
go.mod
The code is short, so I will share the full source below.
counter.go
In counter.go, we define a private struct called singleton with an int field counter. The struct has two methods: Increase and Get.
Because the struct is private, we cannot create it directly from outside the package. We must go through the public function GetInstance, which checks whether an instance already exists. If not, it creates one; otherwise, it returns the existing instance.
package singleton
import "sync"
type singleton struct {
counter int
}
var (
instance *singleton
once sync.Once
)
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{counter: 0}
})
return instance
}
func (s *singleton) Increase() {
s.counter++
}
func (s *singleton) Get() int {
return s.counter
}main.go
In main.go, when we want to increase the counter or read its value, we always go through GetInstance. That guarantees we are using the same Singleton object across the whole program.
/*
Example Singleton
*/
fmt.Println("*** Example Singleton ***")
// User clicks "Print order" for the first time.
// No Counter instance exists yet, so GetInstance creates one (value = 0).
counter.GetInstance().Increase()
// User clicks "Print order" again. We reuse the same instance.
counter.GetInstance().Increase()
fmt.Println("Print count:", counter.GetInstance().Get())
fmt.Print("*** End of Singleton ***\n\n\n")Run the program with:
go run .Output:
*** Example Singleton ***
Print count: 2
*** End of Singleton ***V. Thread Safety
A basic Singleton implementation is not enough on its own. In Go, concurrency is everywhere, so we need to think about thread safety from the start.
A naive lazy initialization like this is not thread-safe:
func GetInstance() *singleton {
if instance == nil {
instance = new(singleton) // race condition when called from multiple goroutines
}
return instance
}If two goroutines call GetInstance at the same time when instance is still nil, both might pass the check and create two different instances. That breaks the Singleton guarantee.
The idiomatic Go fix is sync.Once. It makes sure the initialization function runs exactly once, no matter how many goroutines call it:
var once sync.Once
func GetInstance() *singleton {
once.Do(func() {
instance = &singleton{counter: 0}
})
return instance
}You can verify this with a simple concurrent test:
func TestGetInstanceIsThreadSafe(t *testing.T) {
const goroutines = 100
const increasesPerGoroutine = 100
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
for j := 0; j < increasesPerGoroutine; j++ {
GetInstance().Increase()
}
}()
}
wg.Wait()
expected := goroutines * increasesPerGoroutine
if got := GetInstance().Get(); got != expected {
t.Fatalf("expected %d, got %d", expected, got)
}
}Run the test with:
go test ./singleton/...If everything is thread-safe, the final count should be 10000.
VI. Conclusion
Above, I walked through Singleton — probably the most common design pattern developers learn first. The Counter example is small, but in larger programs, object creation can involve heavy computation or expensive resources. That is when Singleton really pays off.
In this article, we covered a basic Singleton in Golang and made it thread-safe with sync.Once. There are still more topics to explore later — for example, testing Singletons in isolation, or deciding when not to use Singleton at all.
Thank you for reading!
VII. References
- Go Design Patterns (Mario Castro Contreras)
- Full source code for Go design patterns: available here.




