Skip to content
Ownership and lifecycle

goroutineownership

Checks that explicit goroutines have a recognizable join handle or lifecycle owner.

Fix offeredNo

What it detects

Give spawned goroutines a recognizable lifecycle owner. Local producer goroutines must also be able to stop sending when their receiver leaves; use a cancellation-aware select, drain the channel, or provide enough proven buffer capacity for every send.

Checks

Check What it detects
unjoined
Reports goroutines with a recognizable join or lifecycle mechanism that is not honored on every return path.
detached*
Reports goroutines without a recognizable join handle or lifecycle owner.
abandoned-send
Reports producer goroutines that can block after their receiver stops waiting.

* Opt-in; requires explicit selection.

Why this is flagged

A goroutine without a clear owner may keep running after its caller is done or block forever while sending a result nobody will receive. These leaks consume memory and can eventually make shutdowns or the whole program hang.

Further reading: Go concurrency patterns: Pipelines and cancellation, Go concurrency patterns: Context.

How to fix it

Give the goroutine a clear stopping rule: cancel it with a context, join it before returning, or hand it to a longer-lived owner. Make sends cancellation aware so the goroutine can stop if its receiver leaves.

Examples

Flagged code

func refresh() {
// gohawk: goroutine is not joined on every return path
go updateCache()
}

Accepted code

func refreshSafely() {
var group sync.WaitGroup
group.Add(1)
go func() {
defer group.Done()
updateCache()
}()
group.Wait()
}

Options

Knob Default Effect
mode context Ownership policy: context, lifecycle, or join.

By default, a context is enough to own a worker. Use -goroutineownership.mode=lifecycle to require a lifecycle owner, or -goroutineownership.mode=join to require an explicit join.