goroutineownership: Detect unowned goroutines in GoSkip to content
Blog
Concurrency and synchronization

goroutineownership

Checks that proven goroutine completion obligations are honored.

Tiercore
Kindhazard

What it detects

Reports goroutines that signal when they finish, by closing or sending on a channel or calling Done on a sync.WaitGroup, when the launching function does not wait for that signal on every return path.

Waiting counts in a helper, in a select arm, or in a counted loop that receives once from each worker. Work launched without such a signal is not reported, and neither is a worker launched once by main.main, since program exit stops it.

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

Checks

Check Tier Kind What it detects
unjoined
core hazard Reports goroutines with a recognizable join or lifecycle mechanism that is not honored on every return path.

Why this is flagged

A goroutine with an abandoned completion obligation can keep running after its caller returns, or block forever trying to send a result nobody will receive. These leaks hold memory and other resources, and they can make shutdown nondeterministic or hang the program outright.

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

How to fix it

Join the goroutine, transfer its lifecycle explicitly, or connect it to a bounded owner.

Examples

Flagged code

func refresh() {
var group sync.WaitGroup
group.Add(1)
// gohawk: goroutine is not joined on every return path
go func() {
defer group.Done()
updateCache()
}()
}

Accepted code

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