sync.Once & Singleton Patterns
sync.Once runs a function exactly one time, even when many goroutines call it concurrently.
Search across all documentation pages
sync.Once runs a function exactly one time, even when many goroutines call it concurrently.
Teams use it for lazy initialization of expensive resources while avoiding init() ordering hazards.
A singleton exposes one shared instance for the whole program.
Go can implement that with package-level variables, but idiomatic production code prefers explicit dependency injection and reserves sync.Once for cases like registering codecs or loading TLS roots where a single lazy setup is unavoidable.
The stdlib uses sync.Once internally for one-time initialization; your application code should use it sparingly and document why globals are justified.
Quick-reference recipe card - copy-paste ready.
var (
parser *regexp.Regexp
once sync.Once
)
func emailPattern() *regexp.Regexp {
once.Do(func() {
parser = regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`)
})
return parser
}When to reach for this:
main cannot easily pass dependencies into deep call stackspackage metrics
import (
"sync"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
)
type Registry struct {
requests *prometheus.CounterVec
}
var (
defaultReg *Registry
once sync.Once
)
func Default() *Registry {
once.Do(func() {
defaultReg = &Registry{
requests: promauto.NewCounterVec(
prometheus.CounterOpts{Name: "http_requests_total", Help: "HTTP requests"},
[]string{"route", "code"},
),
}
})
return defaultReg
}
func (r *Registry) Inc(route, code string) {
r.requests.WithLabelValues(route, code).Inc()
}
func ExampleUsage() {
Default().Inc("/health", "200")
}What this demonstrates:
once.Do initializes defaultReg on first Default() callDefault() documents the global access pointsync.Once stores a done flag and mutexDo runs f; concurrent waiters block until completionDo becomes a fast atomic loadf panics, Once is stuck - do not panic inside Do| Mechanism | Runs when | Risk |
|---|---|---|
init() | Package load, fixed order | Import side effects, untestable failures |
sync.Once | First explicit call | Hidden global if overused |
Constructor in main | Startup, explicit | Best testability |
| Approach | Testability | Simplicity |
|---|---|---|
Parameters from main | High | Requires wiring |
sync.Once package global | Low | Convenient |
init() global | Lowest | Easiest to write |
// Prefer this in application code:
func NewAPI(reg *metrics.Registry) *API { return &API{reg: reg} }
// In main:
reg := metrics.NewRegistry()
api := NewAPI(reg)once.Do with different functions - only the first runshttp.DefaultClient, prefer explicit client with timeoutOnce never retries; package stays broken. Fix: validate before Do or panic only on programmer errors you accept as fatal.*sql.DB from main or test helper.sync.Once or no init networking.Default() for demos only.sync.Once instead of custom atomics.Once must live on the struct if each instance initializes separately. Fix: package-level Once only for true singletons.| Alternative | Use When | Don't Use When |
|---|---|---|
| Dependency injection | Application services, APIs | Internal regex cache |
init() for pure constants | Registering drivers, math tables | Network or env configuration |
sync.Once lazy init | Costly one-time setup | Every struct field |
Factory in main | Multiple environments (prod/stage) | Library hiding resources |
Package-level mutable singletons are discouraged in application code because they harm tests and hide dependencies.
sync.Once for immutable lazy setup is acceptable in libraries.
No - create a new Once value on a new struct if you need per-instance one-time setup.
After the first call, Do is a single atomic load - negligible on hot paths.
golang.org/x/sync/singleflight deduplicates concurrent work by key - related but not the same as singleton instances.
Only when the ecosystem expects it (http.DefaultClient pattern).
Prefer New returning configured instances.
Refactor to accept interfaces, or test through the public API accepting shared global state as integration tests.
Init order follows import graph; cyclic imports fail.
Keep init() free of I/O.
Eager init in main fails fast at startup; lazy init defers cost until first use and may hide misconfiguration.
Yes - sync.Once remains the standard for one-time setup regardless of generics.
Operators construct one manager in main and pass clients explicitly - preferred over singleton clients.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify at build).
Reviewed by Chris St. John·Last updated Jul 16, 2026