concurrentcapture: Detect unsafe concurrent capture in GoSkip to content
Blog
Concurrency and synchronization

concurrentcapture

Checks locals mutated by goroutines launched repeatedly.

Tiercore
Kindhazard

What it detects

Goroutines launched from a loop should not mutate the same captured local without synchronization. Prefer returning results through a channel or keeping the value local to each goroutine.

The full list of precision boundaries, with the fixtures that pin each one, is in the concurrentcapture design notes.

Checks

Check Tier Kind What it detects
shared-capture
core hazard Reports repeatedly launched goroutines that mutate the same captured local.

Why this is flagged

Those goroutines run at unpredictable times and may read or write the captured value simultaneously. The result is a data race whose output can change from run to run and may only fail under load.

A mutex held around the write suppresses the report only when the lock is held at the write itself. This is a focused hazard check, not a race detector: a capture it accepts is not thereby proven race-free.

Further reading: Data race detector, The Go memory model.

How to fix it

Keep the changing value local to each goroutine and send the finished result back through a channel. If the value truly must be shared, protect every access with the same synchronization mechanism.

Examples

Flagged code

func collect(items []int) error {
var err error
for range items {
go func() {
// gohawk: captured local err is mutated by goroutines launched repeatedly
err = fetch()
}()
}
return err
}

Accepted code

func collectSafely(items []int) error {
var group sync.WaitGroup
errs := make(chan error, len(items))
for range items {
group.Add(1)
go func() {
defer group.Done()
errs <- fetch()
}()
}
group.Wait()
close(errs)
for err := range errs {
if err != nil {
return err
}
}
return nil
}