Synchronization
Overview
The sync package provides low-level synchronization primitives for coordinating goroutines.
sync.Mutex
var (
mu sync.Mutex
count int
)
func increment() {
mu.Lock()
count++
mu.Unlock()
}
// With defer
func safe() {
mu.Lock()
defer mu.Unlock()
// Critical section
}sync.RWMutex
var (
mu sync.RWMutex
data map[string]string
)
func read(key string) string {
mu.RLock()
defer mu.RUnlock()
return data[key]
}
func write(key, value string) {
mu.Lock()
defer mu.Unlock()
data[key] = value
}sync.WaitGroup
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
work(n)
}(i)
}
wg.Wait()sync.Once
var (
once sync.Once
config *Config
)
func getConfig() *Config {
once.Do(func() {
config = loadConfig() // Runs exactly once
})
return config
}sync.Pool
var pool = sync.Pool{
New: func() any {
return make([]byte, 1024)
},
}
func process() {
buf := pool.Get().([]byte)
defer pool.Put(buf)
// Use buf
}sync.Map
var m sync.Map
m.Store("key", "value")
v, ok := m.Load("key")
m.Delete("key")
m.Range(func(k, v any) bool {
fmt.Println(k, v)
return true // Continue iteration
})atomic Package
import "sync/atomic"
var counter int64
atomic.AddInt64(&counter, 1)
value := atomic.LoadInt64(&counter)
atomic.StoreInt64(&counter, 0)Summary
| Type | Purpose |
|---|---|
Mutex |
Exclusive lock |
RWMutex |
Reader/writer lock |
WaitGroup |
Wait for goroutines |
Once |
Single execution |
Pool |
Object reuse |
Map |
Concurrent map |
Worked example
RWMutex cache: many readers, occasional writer.
Save as main.go. Then:
go mod init example
go run .package main
import (
"fmt"
"sync"
)
type Cache struct {
mu sync.RWMutex
data map[string]int
}
func (c *Cache) Get(k string) (int, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[k]
return v, ok
}
func (c *Cache) Set(k string, v int) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[k] = v
}
func main() {
c := &Cache{data: map[string]int{}}
var wg sync.WaitGroup
// Writers
for i := 0; i < 10; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
c.Set(fmt.Sprintf("k%d", n%3), n)
}(i)
}
wg.Wait()
// Readers
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
k := fmt.Sprintf("k%d", n)
v, ok := c.Get(k)
fmt.Printf("get %s -> %d ok=%v\n", k, v, ok)
}(i)
}
wg.Wait()
}Expected output: (values vary; keys present)
get k0 -> ... ok=true
get k1 -> ... ok=true
get k2 -> ... ok=true
More examples
sync.Map for disjoint keys; atomic for a simple counter.
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
var m sync.Map
var hits atomic.Int64
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
m.Store(n%5, n)
hits.Add(1)
}(i)
}
wg.Wait()
count := 0
m.Range(func(_, _ any) bool {
count++
return true
})
fmt.Println("keys:", count, "hits:", hits.Load())
}Expected output:
keys: 5 hits: 100
Runnable example
Save as main.go. Then:
go mod init example
go run .package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
// Mutex-protected counter
var (
mu sync.Mutex
count int
wg sync.WaitGroup
)
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
count++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println("mutex count:", count)
// Once: initialization runs exactly once
var (
once sync.Once
inited int
)
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
once.Do(func() {
inited++
fmt.Println("once.Do ran")
})
}()
}
wg.Wait()
fmt.Println("init count:", inited)
// atomic counter (no mutex)
var atomicCount int64
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
atomic.AddInt64(&atomicCount, 1)
}()
}
wg.Wait()
fmt.Println("atomic count:", atomic.LoadInt64(&atomicCount))
// Pool: reuse a buffer
pool := sync.Pool{
New: func() any {
return make([]byte, 8)
},
}
buf := pool.Get().([]byte)
copy(buf, []byte("go-pool!"))
fmt.Println("pool buffer:", string(buf))
pool.Put(buf)
}Expected output:
mutex count: 1000
once.Do ran
init count: 1
atomic count: 1000
pool buffer: go-pool!
What to notice: Without mu.Lock / atomic, the counters would race. sync.Once guarantees single execution even under concurrent callers. sync.Pool is for short-lived reuse—never assume a put buffer is still yours after Put.
Try next: Remove the mutex around count++ and run go run -race . to watch the race detector fire.