Mutex, RWMutex, WaitGroup, Once, atomic, errgroup.
sync.Once and sync/atomic
sync.Once — do something exactly once
sync.Once.Do(fn) runs fn the first time it's called and never again,
even if multiple goroutines race to call it.
var (
cfg *Config
once sync.Once
)
func Config() *Config {
once.Do(func() {
cfg = loadConfig()
})
return cfg
}
This is the safe way to do lazy initialization. No mutex bookkeeping, no double-checked-locking traps.
You'll see exactly one "init by goroutine N" line — whichever one won the race.
sync/atomic — lock-free counters
For simple counters (and a few other patterns), atomics are even cheaper than a
mutex. The sync/atomic package gives you operations that the CPU performs in
one indivisible step.
In Go 1.19+ the package also has typed wrappers — atomic.Int64, atomic.Bool,
atomic.Pointer[T] — which are nicer to use:
var n atomic.Int64
n.Add(1)
fmt.Println(n.Load())
When to reach for what
| Need | Tool |
| --------------------------------------- | ------------------- |
| Communicate between goroutines | channel |
| Protect shared state | sync.Mutex |
| Read-heavy shared state | sync.RWMutex |
| Wait for N goroutines to finish | sync.WaitGroup |
| Lazy init | sync.Once |
| Lock-free counter / flag | sync/atomic |
| Many-reader concurrent map (rare) | sync.Map |