gleak: detecting leaked goroutines
gleak discovers all running goroutines and fails a test if any "leaked" — i.e. are still running after the test that aren't well-known framework/runtime goroutines and aren't on your ignore list. Docs: https://onsi.github.io/gomega/#gleak-finding-leaked-goroutines. Cross-refs: gomega:async, gomega:matchers.
gleak is an experimental Gomega package.
Import
import (
. "github.com/onsi/gomega"
"github.com/onsi/gomega/gleak" // often dot-imported too, for HaveLeaked/Goroutines/Ignoring*
)
The canonical pattern
Capture a baseline snapshot before the test; assert no leak after. The cleanup runs Eventually(Goroutines).ShouldNot(HaveLeaked(snapshot)):
BeforeEach(func() {
goods := gleak.Goroutines() // snapshot of "good" goroutines, taken now
DeferCleanup(func() {
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(goods))
})
})
Equivalent BeforeEach/AfterEach form (carry the snapshot in a closure variable):
var goods []gleak.Goroutine
BeforeEach(func() { goods = gleak.Goroutines() })
AfterEach(func() { Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(goods)) })
Simplest possible form — no snapshot, relies only on the built-in well-known list:
AfterEach(func() {
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked())
})
Gotchas
ALWAYS use Eventually, never Expect. Goroutines wind down asynchronously; a synchronous Expect races their shutdown and produces false positives. Eventually re-polls until they terminate or it times out (default 1s / 10ms poll — see gomega:async to tune).
Pass Goroutines, not Goroutines(). Note the missing (). Eventually must call it repeatedly on each poll. Writing Eventually(gleak.Goroutines()) snapshots once and defeats the retry. (Calling gleak.Goroutines() with parens is correct only when taking a baseline snapshot to pass into HaveLeaked.)
Capture the baseline at the right time — in BeforeEach, before the test spins anything up, so genuinely pre-existing goroutines are filtered out.
HaveLeaked succeeds when goroutines leaked. A success is a failure of your test — so it's almost always used with ShouldNot/NotTo. The built-in well-known list is always applied and cannot be disabled.
HaveLeaked([ignoring...])
gleak.HaveLeaked(NONLEAKY1, NONLEAKY2, ...)
After filtering out well-known goroutines and everything matched by the ignoring args, it matches if any goroutines remain. Each arg is either a goroutine matcher or a shorthand:
"foo.bar"→IgnoringTopFunction("foo.bar")(exact topmost-function name)"foo.bar..."→ top function name starts with prefixfoo.bar."foo.bar [chan receive]"→ exact top function and goroutine state begins withchan receive[]Goroutine(a snapshot) →IgnoringGoroutines(snapshot), filtered by goroutine ID- any
GomegaMatcherthat operates on an actual of typegleak.Goroutine(e.g.HaveField,WithTransform)
Goroutine matchers (for the ignoring list)
IgnoringTopFunction(name)— matches a goroutine whose topmost stack function isname; supports the"name","prefix...", and"name [state]"forms above.IgnoringInBacktrace(substr)— matches ifsubstrappears anywhere in the backtrace (lazystrings.Contains).IgnoringGoroutines(snapshot)— matches goroutines that are elements ofsnapshot, compared by goroutine ID.IgnoringCreator(name)— matches by the name of the function that created the goroutine; supports"name"and"prefix..."forms.
Eventually(gleak.Goroutines).ShouldNot(gleak.HaveLeaked(
gleak.IgnoringTopFunction("github.com/me/pkg.worker"),
gleak.IgnoringInBacktrace("github.com/some/dep.background"),
goods, // the snapshot, as IgnoringGoroutines shorthand
))
Ginkgo -p (parallel)
Running with ginkgo -p adds a background goroutine for Ginkgo↔package communication that will look leaked. Call gleak.IgnoreGinkgoParallelClient() at the start of each package's BeforeSuite so gleak adds it to the ignore list:
var _ = BeforeSuite(func() {
gleak.IgnoreGinkgoParallelClient()
})
Well-known non-leaky goroutines (ignored by default)
Always filtered, by topmost function name (or backtrace), so you don't list them:
- signal handling:
os/signal.signal_recv,os/signal.loop,runtime.ensureSigM - Go
testing:testing.RunTests,testing.(*T).Run,testing.(*T).Parallel(all[chan receive]) - Ginkgo internals:
(*Suite).runNode, the interrupt-handler/progress-signal goroutines, output-interceptorResumeIntercepting, and the v1 spec-runner interrupt handler - anything with
runtime.ReadTracein its backtrace
Reporting
On a leak, gleak prints only the leaked goroutines (not all, unlike a panic), in a compact one-line-per-frame form. This output is not subject to format.MaxLength. By default locations show package + file + line (main.foo.func1() at foo/bar.go:123). Set gleak.ReportFilenameWithPath = true for full absolute paths.
Goroutine IDs
gleak.Goroutine values carry a goroutine ID (goid) parsed from the runtime stack; IgnoringGoroutines matches snapshots by these IDs. IDs are not reused (barring 64-bit counter wraparound) but are not densely sequential. Use them for testing/debugging only — never for program logic.