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?