Table of Contents
- Concurrency
- Contents
- Goroutines
- Channels
- Channel axioms
- Directional channels
- select
- sync primitives
- atomic
- context
- Worker pool
- Fan-out / fan-in
- errgroup
- Race detector & rules
Concurrency¶
Contents¶
- Goroutines
- Channels
- Channel axioms
- Directional channels
- select
- sync primitives
- atomic
- context
- Worker pool
- Fan-out / fan-in
- errgroup
- Race detector & rules
Goroutines¶
A lightweight thread managed by the runtime. go f() starts one; it returns immediately.
go doWork()
go func(id int) { fmt.Println(id) }(1) // pass args, don't capture loop vars carelessly
// main returns -> program exits, killing all goroutines. You must synchronize.
Go 1.22+: loop variables are per-iteration, so for i := range 3 { go func(){ use(i) }() } is safe. Pre-1.22 you'd copy i := i.
Channels¶
Typed conduits. Send ch <- v, receive v := <-ch. Communication synchronizes.
ch := make(chan int) // unbuffered: send blocks until a receiver is ready
buf := make(chan int, 10) // buffered: send blocks only when full
ch <- 5 // send
v := <-ch // receive
v, ok := <-ch // ok=false if channel closed AND drained
close(ch) // no more sends allowed
len(buf) // queued elements
cap(buf) // buffer size
for v := range ch { // ranges until ch is closed (and drained)
use(v)
}
Unbuffered = synchronous handoff (rendezvous). Buffered = async up to capacity.
Channel axioms¶
Memorize these — they are the source of most concurrency bugs:
| Operation | nil channel | open channel | closed channel |
|---|---|---|---|
send ch <- x |
blocks forever | ok / blocks if full | panic |
receive <-ch |
blocks forever | ok / blocks if empty | returns zero value, ok=false |
close(ch) |
panic | ok | panic |
// Receiving from a closed channel never blocks and yields the zero value:
ch := make(chan int)
close(ch)
v, ok := <-ch // v=0, ok=false
// Sending on a closed channel panics:
close(ch); ch <- 1 // panic: send on closed channel
// Closing twice panics:
close(ch); close(ch) // panic: close of closed channel
Rules of thumb:
- Only the sender closes a channel, never the receiver.
- Closing is a broadcast: all receivers see it. Use a
chan struct{}closed as a "done"/"stop" signal. - You don't have to close a channel; GC reclaims it. Close only to signal "no more values."
- A nil channel disables a
selectcase (useful trick).
Directional channels¶
Restrict direction in function signatures for safety/clarity.
func produce(out chan<- int) { out <- 1 } // send-only
func consume(in <-chan int) { <-in } // receive-only
ch := make(chan int)
produce(ch) // bidirectional converts to send-only automatically
consume(ch)
select¶
Waits on multiple channel ops; picks a ready one (random among ready).
select {
case v := <-ch1:
use(v)
case ch2 <- x:
sent()
case <-time.After(time.Second):
timeout() // timeout pattern
default:
// runs if NO case is ready -> non-blocking select
}
// Loop with a stop signal:
for {
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
process(job)
}
}
// Disable a case by nilling its channel:
var c chan int // nil; this case never fires until reassigned
select {
case <-c: // blocked forever (nil) -> effectively disabled
case <-other:
}
sync primitives¶
import "sync"
// Mutex: mutual exclusion
var mu sync.Mutex
mu.Lock()
defer mu.Unlock()
// critical section
// RWMutex: many readers OR one writer
var rw sync.RWMutex
rw.RLock(); _ = data; rw.RUnlock() // concurrent reads
rw.Lock(); data = x; rw.Unlock() // exclusive write
// WaitGroup: wait for N goroutines
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1) // BEFORE go, not inside
go func(it Item) {
defer wg.Done()
process(it)
}(item)
}
wg.Wait() // blocks until counter hits 0
// Once: run exactly once (lazy init, singletons)
var once sync.Once
once.Do(func() { initExpensive() }) // safe across goroutines
// Pool: reuse allocations to reduce GC pressure
var bufPool = sync.Pool{New: func() any { return new(bytes.Buffer) }}
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
defer bufPool.Put(b)
// sync.Map: concurrent map for append-heavy / disjoint-key workloads
var m sync.Map
m.Store("k", 1)
v, ok := m.Load("k")
m.LoadOrStore("k", 2)
m.Range(func(k, v any) bool { return true })
Never copy a sync.Mutex/WaitGroup/Once after use — pass by pointer (or embed in a struct passed by pointer). go vet catches lock copies.
atomic¶
Lock-free operations on integers/pointers. Prefer the typed wrappers (Go 1.19+).
import "sync/atomic"
var counter atomic.Int64
counter.Add(1)
counter.Load()
counter.Store(10)
counter.CompareAndSwap(10, 20) // CAS
old := counter.Swap(5)
var flag atomic.Bool
flag.Store(true)
var p atomic.Pointer[Config]
p.Store(&Config{})
cfg := p.Load()
// Old function form (still common):
atomic.AddInt64(&n, 1)
atomic.LoadInt64(&n)
Use atomics for simple counters/flags; use mutexes when multiple fields must change together.
context¶
Carries cancellation, deadlines, and request-scoped values across API boundaries. Pass as the first param: ctx context.Context.
import "context"
ctx := context.Background() // root (main, init, tests)
ctx = context.TODO() // placeholder when unsure
ctx, cancel := context.WithCancel(ctx)
defer cancel() // ALWAYS call cancel to release resources
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Minute))
// Respond to cancellation:
select {
case <-ctx.Done():
return ctx.Err() // context.Canceled or context.DeadlineExceeded
case res := <-work:
return res
}
// Request-scoped values (use sparingly; typed keys to avoid collisions):
type ctxKey struct{}
ctx = context.WithValue(ctx, ctxKey{}, "req-123")
id, _ := ctx.Value(ctxKey{}).(string)
Propagate ctx down the call tree; never store it in a struct field (except short-lived request objects). Always defer cancel().
Worker pool¶
Bounded concurrency over a stream of jobs.
func workerPool(jobs []int, workers int) []int {
in := make(chan int)
out := make(chan int)
var wg sync.WaitGroup
for w := 0; w < workers; w++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := range in { // each worker pulls until `in` closed
out <- j * j
}
}()
}
go func() { // feed jobs
for _, j := range jobs { in <- j }
close(in) // signal workers: no more jobs
}()
go func() { wg.Wait(); close(out) }() // close out after all workers done
var results []int
for r := range out { results = append(results, r) }
return results
}
Fan-out / fan-in¶
// Fan-out: multiple goroutines read from one channel (the pool above).
// Fan-in: merge multiple channels into one.
func merge(cs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range cs {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c { out <- v }
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}
errgroup¶
golang.org/x/sync/errgroup: run goroutines, collect the first error, cancel the rest.
import "golang.org/x/sync/errgroup"
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10) // optional: cap concurrency
for _, url := range urls {
url := url
g.Go(func() error {
return fetch(ctx, url) // ctx is cancelled when any returns error
})
}
if err := g.Wait(); err != nil { // first non-nil error
return err
}
Race detector & rules¶
Core rules:
- Don't communicate by sharing memory; share memory by communicating (use channels) — or guard shared memory with a mutex/atomic.
- Any concurrent read+write of the same variable without synchronization is a data race (undefined behavior).
- The race detector finds races at runtime only on code paths actually exercised; add tests.
WaitGroup.Addmust happen before the correspondinggo;Donein adefer.