producerlifecycle
Checks that goroutine producers cannot outlive their receivers.
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}