The sync package
Channels are not the only tool. For simple synchronization tasks
the standard library provides the sync package.
sync.WaitGroup
Waiting for a group of goroutines to finish:
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println("done:", n)
}(i)
}
wg.Wait()
In Go 1.25+ there is a shorter form: wg.Go(func() { ... }).
sync.Mutex
Protecting shared data from races:
type Counter struct {
mu sync.Mutex
n int
}
func (c *Counter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
sync.RWMutex lets many readers work at the same time
(RLock/RUnlock), while a writer gets exclusive access.
sync.Once
Guarantees a single execution, e.g. for lazy initialization:
var (
once sync.Once
cfg *Config
)
func GetConfig() *Config {
once.Do(func() {
cfg = loadConfig()
})
return cfg
}
sync/atomic
Atomic types are handy for simple counters:
var hits atomic.Int64
hits.Add(1)
fmt.Println(hits.Load())
Channels or mutexes?
- Channels — transferring data ownership, pipelines, event signals.
- Mutexes — protecting state inside a struct (a cache, a counter).