🚗 Analogy
A moving car has exactly one driver at the wheel. Everyone inside — and everything the car does — answers to that single driver, and there’s one well-known seat to reach them. You can’t have two people steering at once.
The problem
Some things should exist once per program: a configuration loaded from the environment, a database connection pool, a shared logger. If every caller created its own, you’d get inconsistent config, exhausted connections, or interleaved log files.
You need two guarantees: only one instance is ever created, and everyone can reach that same instance through one access point. And in Go specifically: initialization must be safe when many goroutines ask for the instance at once.
Structure
classDiagram
class Singleton {
-instance Singleton
+GetInstance() Singleton
+SomeMethod()
}
note for Singleton "GetInstance() always returns the same instance. The constructor is unexported."
Callers never construct the type directly — they go through a single accessor that hands back the one shared value.
How it works
sequenceDiagram
participant G1 as Goroutine 1
participant G2 as Goroutine 2
participant S as GetInstance / sync.Once
G1->>S: GetInstance()
S->>S: once.Do creates instance (runs ONCE)
S-->>G1: *instance
G2->>S: GetInstance()
S-->>G2: *instance (same pointer, no re-init)
Idiomatic Go
Go’s standard library hands you the perfect tool: sync.Once. Its Do method runs a function exactly once, no matter how many goroutines call it concurrently — and it’s faster than locking on every access.
package config
import "sync"
type Config struct {
DSN string
Debug bool
}
var (
instance *Config
once sync.Once
)
// Get returns the one shared Config, initializing it on first use.
// Safe to call from many goroutines at once.
func Get() *Config {
once.Do(func() {
instance = &Config{DSN: "postgres://localhost/app"}
})
return instance
}
Try it — this version spins up five goroutines that all race to initialize, then proves they got the same instance. Edit it and hit Run:
package main
import (
"fmt"
"sync"
)
type Config struct {
DSN string
Debug bool
}
var (
instance *Config
once sync.Once
)
func Get() *Config {
once.Do(func() {
fmt.Println("initializing config (this prints once)")
instance = &Config{DSN: "postgres://localhost/app"}
})
return instance
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() { defer wg.Done(); _ = Get() }()
}
wg.Wait()
a, b := Get(), Get()
fmt.Printf("same instance: %v\n", a == b)
fmt.Println("DSN:", a.DSN)
}
💡 Even simpler: eager init
If the instance is cheap and always needed, skip the laziness — a package-level variable initialized at declaration is a singleton, and Go guarantees package initialization runs once before main.
var instance = &Config{DSN: "postgres://localhost/app"} // created once, at startup
func Get() *Config { return instance }
A parameterized variation
One variation passes the *sync.Once in as a parameter and even recovers from panics during construction — a nice way to see the mechanics laid bare. In production you’d usually keep the sync.Once as an unexported package-level variable instead.
In the standard library
sync.Onceitself is the canonical building block.http.DefaultClient,http.DefaultServeMuxare package-level singletons you use every day.- Go’s package initialization (
init()+ package-level vars) is a language-level singleton guarantee.
Pitfalls
⚠️ Singleton is global state wearing a tie
The pattern’s biggest danger isn’t concurrency — sync.Once handles that. It’s that a singleton is hidden, shared, mutable state. It couples every caller to one concrete value, makes unit tests share state across runs, and resists mocking. Before reaching for it, ask: “could I just pass this in as a dependency?” Often the answer is yes, and your code gets more testable for it.
When to use it — and when not
✅ Reach for it when
- There must be exactly one instance — a process-wide config, a connection pool, a logger, a metrics registry.
- That single instance needs a clear, shared access point across the program.
- Lazy, one-time initialization needs to be safe under concurrency.
⛔ Think twice when
- You're really just reaching for a global variable — singletons hide global mutable state and make tests order-dependent.
- You need to swap the implementation in tests; a singleton is hard to mock. Prefer passing a dependency in.
- Multiple configurations might be needed later — a singleton bakes 'exactly one' into your architecture.
Related patterns
Define an interface for creating an object, but let the implementation decide which concrete type to instantiate.
CREATIONAL Abstract FactoryProvide an interface for creating families of related objects without specifying their concrete types.
STRUCTURAL FacadeProvide a single, simplified interface over a complex subsystem, so clients don't have to orchestrate its parts.
Check your understanding
Score: 0 / 31. What is the idiomatic, concurrency-safe way to build a lazy singleton in Go?
sync.Once guarantees the init function runs exactly once, even under concurrent calls, with no race — and no lock on the hot path after the first call.
2. What's the main design risk of the Singleton pattern?
A singleton is global state in disguise. It couples callers to a concrete instance and makes tests share state — often better to inject the dependency.
3. Why is a bare `if instance == nil { instance = ... }` unsafe?
Without synchronization, concurrent callers race on the check-and-set, possibly creating two instances and tripping the race detector.