producerlifecycle: Detect unbounded goroutine producers in GoSkip to content
Blog
Concurrency and synchronization

producerlifecycle

Checks that goroutine producers cannot outlive their receivers.

Tierscore, experimental
Kindhazard

What it detects

Reports producer goroutines that can block after their receiver stops waiting.

The core check proves the count: a local unbuffered channel, a receiver that stops after a known number of receives, and more sends than that. Buffered and externally supplied channels, send counts that depend on a loop, and channels that code the analyzer cannot see may also drain are left unreported.

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

Checks

Check Tier Kind What it detects
abandoned-send
core hazard Reports producer goroutines that can block after their receiver stops waiting.
stopped-loop-send
experimental hazard Reports sends that can block forever after the service loop receiving them returns.
unclosed-range
experimental hazard Reports range loops that wait forever when their producer returns an error without closing the channel.

Why this is flagged

A producer goroutine that sends on an unbuffered channel blocks until something receives. If the consumer stops early — an error, an early return, a cancelled context — a producer that keeps trying to send has no one to receive it and blocks forever, leaking the goroutine and whatever it holds. Making the send cancellation-aware, or draining the channel, gives the producer a way to stop.

Further reading: Go concurrency patterns: Pipelines and cancellation.

How to fix it

Drain every result, provide a proven buffer, or make sends cancellation-aware.

Examples

Flagged code

Producer outlives its receiver

func firstResultOnly() error {
results := make(chan error)
go func() {
results <- errors.New("first")
// gohawk: goroutine send can block after the receiver stops waiting
results <- errors.New("second")
}()
return <-results
}

Send after a service loop stops

type scheduler struct {
add chan int
stop chan struct{}
}
func (s *scheduler) run() {
for {
select {
case <-s.add:
case <-s.stop:
return
}
}
}
func (s *scheduler) Schedule(v int) {
// gohawk: send can block forever after the service loop receiving it returns
s.add <- v
}
func (s *scheduler) Stop() { close(s.stop) }

Range waits on a failed producer

type exporter struct{ rows chan string }
func (e *exporter) run(fail bool) error {
if fail {
return errors.New("export failed")
}
e.rows <- "row"
close(e.rows)
return nil
}
func printRows(e *exporter, fail bool) {
go func() { _ = e.run(fail) }()
// gohawk: range can wait forever: run returns an error without closing the channel
for row := range e.rows {
println(row)
}
}

Accepted code

func drainResults() {
results := make(chan error)
go func() {
results <- errors.New("first")
results <- errors.New("second")
}()
<-results
<-results
}