Concurrency#
Go’s concurrency model is goroutines + channels +
context. A goroutine is a lightweight thread the runtime
schedules across OS threads (M:N scheduler); channels carry
typed values between goroutines; context carries
cancellation and deadlines across function boundaries.
/operators`.
For the surrounding control-flow constructs (select,
defer), see Control flow.
Goroutines#
go f() schedules f to run concurrently. Cheap (a few KB
of stack each); the operator launches thousands without worry.
go worker(id)
for _, item := range items {
go process(item) // unbounded; usually wrong
}
Launching a goroutine and forgetting about it leaks; the
operator either waits on it (with sync.WaitGroup or
errgroup) or owns its lifecycle through a context.
Channels#
A typed pipe between goroutines.
ch := make(chan int) // unbuffered (synchronous)
ch := make(chan int, 10) // buffered (async until full)
ch <- 42 // send (blocks if unbuffered + no receiver)
v := <-ch // receive
v, ok := <-ch // ok=false when ch is closed and drained
close(ch) // sender closes; receiver detects
A closed channel returns the zero value forever; receiving from a nil channel blocks forever. Never closes a channel from the receiver side.
Direction in signatures.
func producer(out chan<- int) { out <- 1 }
func consumer(in <-chan int) { fmt.Println(<-in) }
select#
select waits on multiple channel operations; the first
ready case runs. Random if several are ready.
select {
case v := <-ch:
handle(v)
case ch <- next:
sent()
case <-ctx.Done():
return ctx.Err()
case <-time.After(5 * time.Second):
return errors.New("timeout")
default:
// non-blocking; runs if no case is ready
}
sync.WaitGroup#
Wait for a known set of goroutines to finish.
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
process(item)
}(item)
}
wg.Wait()
For Go 1.22+, the loop variable is per-iteration; in older Go
the operator passed item into the goroutine explicitly (as
above) to avoid the closure trap.
errgroup#
golang.org/x/sync/errgroup is WaitGroup plus typed
errors plus context cancellation. Go-tos for
fan-out with shared cancellation.
import "golang.org/x/sync/errgroup"
g, ctx := errgroup.WithContext(ctx)
for _, url := range urls {
url := url // pre-1.22 loop-variable capture
g.Go(func() error {
return fetch(ctx, url) // ctx cancelled on first error
})
}
if err := g.Wait(); err != nil { return err }
g.SetLimit(n) caps in-flight goroutines.
context#
context.Context carries cancellation, deadlines, and
request-scoped values across API boundaries. The operator
threads ctx as the first parameter of every function that
might block.
ctx, cancel := context.WithTimeout(ctx, 5 * time.Second)
defer cancel()
if err := op(ctx); err != nil { return err }
Common constructors.
Constructor |
Effect |
|---|---|
|
The root context. Use at |
|
Placeholder while the operator decides where the real context comes from. |
|
Returns a child cancellable by calling the returned
|
|
Cancels after |
|
Cancels at absolute time |
|
Attach a request-scoped value. Use a typed key, sparingly. |
A function honours the context by selecting on ctx.Done().
func op(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
return nil
case <-ctx.Done():
return ctx.Err()
}
}
Mutexes#
For shared mutable state that does not fit cleanly into a
channel model, sync.Mutex and sync.RWMutex are the
primitives.
type Cache struct {
mu sync.Mutex
data map[string]string
}
func (c *Cache) Get(k string) (string, bool) {
c.mu.Lock()
defer c.mu.Unlock()
v, ok := c.data[k]
return v, ok
}
func (c *Cache) Set(k, v string) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[k] = v
}
sync.RWMutex is the read-mostly variant; RLock /
RUnlock allow many concurrent readers.
sync.Once#
Lazy initialisation, exactly once.
var (
loaderOnce sync.Once
loader *Loader
)
func getLoader() *Loader {
loaderOnce.Do(func() { loader = NewLoader() })
return loader
}
Atomic#
sync/atomic exposes lock-free primitives for counters and
flags. The Go 1.19+ typed atomics (atomic.Int64,
atomic.Bool, atomic.Pointer[T]) replace the older
function-form API.
var counter atomic.Int64
counter.Add(1)
fmt.Println(counter.Load())
The race detector#
go test -race and go build -race enable runtime race
detection. Run every CI test suite with
-race; it catches concurrent reads / writes on the same
location.
References#
Operators for
<-send / receive semantics.Control flow for
selectanddefer.Errors for
panic/recoverat goroutine boundaries.