Learn Go← Dashboard

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.

go playground
Loading...

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.

go playground
Loading...

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 |

When is `sync/atomic` the right choice over `sync.Mutex`?