sync and sync/atomic
sync and sync/atomic
Overview
When goroutines share memory, the stdlib gives two complementary tools:
| Package | Style |
|---|---|
sync |
Mutexes, WaitGroups, Once, Cond, Map, Pool |
sync/atomic |
Lock-free counters and pointers for simple cases |
Prefer channels for ownership transfer and mutexes for shared structures. Concurrency design: Part 07.
WaitGroup
var wg sync.WaitGroup
for _, job := range jobs {
wg.Add(1)
go func(j Job) {
defer wg.Done()
process(j)
}(job)
}
wg.Wait()Call Add before spawning; never negative. Prefer wg.Go (Go 1.25+) when available:
wg.Go(func() { process(job) })Mutex and RWMutex
type Cache struct {
mu sync.Mutex
m map[string]string
}
func (c *Cache) Get(k string) (string, bool) {
c.mu.Lock()
defer c.mu.Unlock()
v, ok := c.m[k]
return v, ok
}RWMutex helps read-heavy maps; measure before assuming it wins.
Once
var once sync.Once
var client *http.Client
func Client() *http.Client {
once.Do(func() {
client = &http.Client{Timeout: 10 * time.Second}
})
return client
}Pool
var bufPool = sync.Pool{
New: func() any { return make([]byte, 32*1024) },
}
b := bufPool.Get().([]byte)
defer bufPool.Put(b[:cap(b)]) // reset length if you reslicedUse for high-churn buffers; do not assume pooled items are zeroed.
Map
sync.Map is specialized (append-only keys, many disjoint keys). For ordinary caches, a mutex + map is clearer.
Cond (advanced)
mu := sync.Mutex{}
cond := sync.NewCond(&mu)
// Waiters: cond.Wait(); Signal/Broadcast after state change under the same mutexSee pipelines / Cond.
atomic
var hits atomic.Int64
hits.Add(1)
fmt.Println(hits.Load())
var flag atomic.Bool
flag.Store(true)Typed atomics (atomic.Int64, Bool, Pointer[T]) are preferred over AddInt64 free functions.
Compare-and-swap
var p atomic.Pointer[Config]
p.Store(&Config{V: 1})
for {
old := p.Load()
next := *old
next.V++
if p.CompareAndSwap(old, &next) {
break
}
}Race detector
go test -race ./...
go run -race .If the race detector complains, fix the ownership story — do not “hope” atomics alone fix complex structs.
Runnable example
go mod init example
go run .package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var hits atomic.Int64
var wg sync.WaitGroup
var mu sync.Mutex
seen := make(map[int]bool)
for i := 0; i < 8; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
hits.Add(1)
mu.Lock()
seen[id] = true
mu.Unlock()
}(i)
}
wg.Wait()
fmt.Println("hits", hits.Load())
fmt.Println("unique", len(seen))
var once sync.Once
once.Do(func() { fmt.Println("init once") })
once.Do(func() { fmt.Println("never again") })
}Expected output:
hits 8
unique 8
init once
What to notice: - Counters can be atomic; maps still need a mutex (or channels). - Once runs exactly one successful init path. - Run with -race to validate this pattern under the detector.
Try next: Replace the mutex map with a channel that owns the map in a single goroutine (share-by-communication).